多线程- NSThread、GCD、NSOperation/NSOperationQueue

一、多线程简介:

所谓多线程是指一个 进程 -- process可以理解为系统中正在运行的一个应用程序)中可以开启多条 线程 -- thread线程是进程的基本执行单元,一个进程的所有任务都在线程中执行,每1个进程至少要有1条线程),多条线程可以同时执行不同的任务 -- task。CPU每一个核同一时间只能执行一条线程。多线程对于单核来讲就是让CPU快速的在多个线程之间进行调度(并发)。而多核处理系统可以让多个线程实现真正的同时进行(并行)。

多线程优点:

  • 可以提高应用程序的执行效率。
  • 可以提高应用程序在多核系统上的实时性能,提高资源利用率。

多线程缺点:

  • 开启线程需要一定的内存空间,默认的,主线程占用1M,一条子线程占用栈区内存512KB,同时,线程过多会导致CPU在线程上调度上的开销比较大。
  • 在应用程序中执行多个路径可能会为代码增加相当多的复杂性。同时,每个线程必须与其他线程协调其动作,以防止它破坏应用程序的状态信息。因为单个应用程序中的线程共享相同的内存空间,所以它们可以访问所有相同的数据结构。如果两个线程同时操作相同的数据结构,一个线程可能会以破坏生成的数据结构的方式覆盖其他线程的更改。

在iOS中,一个iOS程序运行后,默认会开启1条线程,称为“主线程”或“UI线程”,主线程的主要作用是显示\刷新UI界面、处理UI事件,如点击、滚动、拖拽等事件。需要注意的是:别将比较耗时的操作放到主线程中,会影响程序流畅度。

iOS中常见的多线程技术一般有三个:NSThread、NSOperation、GCD,其中GCD是目前苹果官方比较推荐的方式。

二、NSThread:

NSThread可以直接操控线程对象,非常直观和方便。但是它的生命周期有时需要我们手动管理,一般用来查看当前线程是否是主线程。下面来看看它的一些常见用法:

  • @property (class, readonly, strong) NSThread *currentThread;: 获取当前线程。一般与isMainThread结合判断当前线程是否为主线程。

  • + (void)detachNewThreadWithBlock:(void (^)(void))block:采用block形式创建一个新的线程,创建后自动启动,iOS10.0系统以后才能使用。

  • + (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;:采用方法选择器方法创建线程,创建后自动启动,可以传递参数。

  • @property (readonly, retain) NSMutableDictionary *threadDictionary;:每个线程都维护一个在线程任何地方都能获取的字典。获取一个NSMutableDictionary对象后,可以添加我们需要的字段和数据。默认是空的。注意这里是retain!

  • + (void)sleepUntilDate:(NSDate *)date;+ (void)sleepForTimeInterval:(NSTimeInterval)ti;:阻塞线程,需要注意的是:线程死了不能复生!

  • + (void)exit;:退出当前线程。

  • + (double)threadPriority;+ (BOOL)setThreadPriority:(double)p;:设置线程优先级,线程优先级的取值范围为0.0~1.0之间,1.0表示线程的优先级最高,默认为0.5。需要注意的是,不是优先级高就会先走当前线程,结束后再走其他线程,依然是来回调用,但是优先级高的线程,调用的概率、频率会很大。

  • @property double threadPriority:优先级,将要被废弃,可以使用下面的属性替代。

  • @property NSQualityOfService qualityOfService:该属性,8.0后才能使用。其中NSQualityOfService是枚举类型。

// 以下服务质量(QoS)用于向系统表明工作的性质和重要性。 它们被系统用于管理各种资源。
// 资源争夺期间,较高的QoS类别获得更多的资源(除default外优先级依次降低)
typedef NS_ENUM(NSInteger, NSQualityOfService) {
    //用于直接涉及提供交互式UI的工作,例如处理事件或绘图到屏幕
    NSQualityOfServiceUserInteractive = 0x21,
    
    //用于执行用户明确请求的工作,并且必须立即呈现哪些结果,以便进一步进行用户交互。
    NSQualityOfServiceUserInitiated = 0x19,
    
    //用于执行用户不太可能立即等待结果的工作。
    //这项工作将以节能的方式运行,在资源受到限制时尊重更高的QoS工作。
    NSQualityOfServiceUtility = 0x11,
    
    //用于非用户启动或可见的工作。
    //它将以最有效的方式运行,同时最大限度地尊重更高的QoS工作。
    NSQualityOfServiceBackground = 0x09,

    //表示缺少QoS信息。每当有可能的QoS信息,将从其他来源推断。
    //如果这样的推断是不可能的,则使用UserInitiated和Utility之间的QoS。
    NSQualityOfServiceDefault = -1
} NS_ENUM_AVAILABLE(10_10, 8_0);
  • @property (nullable, copy) NSString *name:设置线程名字。

  • @property NSUInteger stackSize:设定新的栈空间大小。栈空间的大小必须在线程的启动之前设定,即在调用NSThread的start方法之前通过setStackSize: 设置。

  • @property (readonly) BOOL isMainThread/- (BOOL)isMainThread;/+ (BOOL)isMainThread;:判断当前线程是否为主线程。

  • + (NSThread *)mainThread;:获取主线程。

  • - (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument:创建线程,需要调用-start方法才会开启线程。

  • - (instancetype)initWithBlock:(void (^)(void))block:采用block形式创建一个新的线程,同样需要调用-start方法才会开启线程,10.0以后才能使用。

  • @property (readonly, getter=isExecuting) BOOL executing:判断线程是否正在执行。

  • @property (readonly, getter=isFinished) BOOL finished:判断线程是否已经完成。

  • @property (readonly, getter=isCancelled) BOOL cancelled:判断线程是否已经被取消。

  • - (void)cancel:取消线程。

  • - (void)start: 开始线程。

  • - (void)main:线程入口函数(包含start功能)。 相当于重新走一次线程。

基于NSThread来实现的NSObject的扩展方法:

  • - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;:在主线程中执行,wait表示是否线程任务完成执行,array:指定了线程中 Runloop 的 Modes(有两种:kCFRunLoopDefaultModekCFRunLoopCommonModes)。

  • - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;:相当于上面的方法指定modes为kCFRunLoopCommonModes

  • - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array / - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait:在指定的线程中运行。

以上四个方法常用于线程间的通讯


  • - (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg:所有对象都有能力产生一个新的线程并使用它来执行其中的方法。这一方法隐式的创建一个新的线程并执行其方法。

二、GCD:

Grand Central Dispatch (GCD)是异步执行任务的技术之一,它使用了非常简洁的方法实现了极为复杂繁琐的多线编程。

1、队列的理解:
  • 调度队列 -- Dispatch Queues :显示如何在应用程序中同时执行任务。一个调度队列会顺序或同时却始终执行一个先入先出(FIFO)的顺序任务。(换句话说,调度队列总是以与队列中相同的顺序进行队列启动任务。)调度队列用dispatch_queue_t 类型表示,其创建函数为dispatch_queue_t dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attire);,其中第一个参数用于指定队列名称,一般用 逆序全程域名+对象名 的格式来命名,第二个参数是调度队列的属性可以传:
    • DISPATCH_QUEUE_SERIAL/NULL -- 创建活跃的(不需要手动激活的)串行队列;
    • DISPATCH_QUEUE_CONCURRENT -- 创建活跃的并行队列;
    • DISPATCH_QUEUE_SERIAL_INACTIVE -- 创建非活跃状态的串行队列,需要调用 dispatch_activate()函数激活,未激活队列无法使用。可以使用dispatch_set_target_queue()更改处于非活动状态的队列的优先级。一旦最初不活动的队列被激活,优先级的更改就不再允许。
    • DISPATCH_QUEUE_CONCURRENT_INACTIVE -- 创建非活跃状态的并行队列`

调度队列一般有两种类型:

  • 串行调度队列 -- Serial Dispatch Queues :也称为 private dispatch queues ,按照任务被添加到队列的顺序一次执行一个任务。串行队列通常用于同步对特定资源的访问。可以根据需要创建任意数量的串行队列,并且每个队列相对于所有其他队列并发运行。也就是说,如果你创建四个串行队列,则每个队列一次只会执行一个任务,但最多可以同时执行四个任务。

  • 并发调度队列 -- Concurrent Dispatch Queues,这是全局队列 global dispatch queue 的一种,同时执行一个或多个任务,但是任务仍按照添加到队列的顺序启动。当前执行的任务在由调度队列管理的不同线程上运行。

除了自己创建队列外,还可以获取系统提供的队列:

  • 主调度队列 -- Main Dispatch Queue,主调度队列是一个全局可用的 串行队列,用于执行应用程序主线程上的任务。该队列与应用程序的运行循环(runLoop)一起工作。

    • 获取方法:dispatch_queue_t mainQueue = dispatch_get_main_queue();
  • 全局队列 -- Global Dispatch Queue,是所有应用程序都能够使用的并发调度队列(Concurrent Dispatch Queues)。

    • 获取方法:dispatch_queue_t dispatch_get_global_queue(long identifier, unsigned long flags);
      • 当使用此函数返回的队列时调用dispatch_suspend() -- 挂起某个队列,dispatch_resume() -- 恢复某个队列,dispatch_set_context() -- 等函数将不起作用。
      • 参数identifier可以使用qos_class_t中定义的服务质量类或dispatch_queue_priority_t中定义的优先级。下面是其常量定义与对应关系:
        • QOS_CLASS_USER_INTERACTIVE :用户交互即便额
        • QOS_CLASS_USER_INITIATED -- DISPATCH_QUEUE_PRIORITY_HIGH : 高优先级
        • QOS_CLASS_DEFAULT -- DISPATCH_QUEUE_PRIORITY_DEFAULT:默认优先级
        • QOS_CLASS_UTILITY -- DISPATCH_QUEUE_PRIORITY_LOW:低优先级
        • QOS_CLASS_BACKGROUND --DISPATCH_QUEUE_PRIORITY_BACKGROUND`:后台执行
      • 参数flags:现阶段暂时用不到,一般传 0。
2、GCD中有2个用来执行任务的函数:
  • void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);:异步执行任务。

  • void dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);:同步执行任务。

关于同步、异步、并发、串行的概念,简单来讲同步异步是针对是否开辟新线程来讲的,而并发串行是针对队列中任务的执行顺序来讲的,关于具体概念及关系:

  • 同步 -- sync: 只能在当前线程按先后顺序依次执行,不开启新线程。
  • 异步 -- async: 可以在当前线程开启多个新线程执行,可不按顺序执行。
  • 并发 -- concurrency: 线程执行可以同时一起进行执行。
  • 串行 -- serial: 线程执行只能依次逐一先后有序的执行。
🍎 系统全局队列 系统主队列 creat串行队列 creat并发队列
同步(sync),均不开辟新线程,在本线程上运行 不能这样使用 不能这样使用,会造成死锁!!! 在某一线程上串行 在某一线程上串行
异步(async) 每次都会开辟新线程,并发 不开辟新线程,在某一线程上串行,独立直接使用就是主线程上 会开辟一条新线程,在新的线程上串行 每次都会开辟新线程,并发

可以如下示例一样去验证以加深理解:

    NSLog(@"主线程 == %@", [NSThread currentThread]);
    // 调度队列类型 dispatch_queue_t
    dispatch_queue_t serialQueue, concurrentQueue, mainQueue, globalQueue;
    // 调度队列创建函数创建串行队列
    serialQueue = dispatch_queue_create("com.atx610.NewMTX.serialQueue", DISPATCH_QUEUE_SERIAL);
    // 调度队列创建函数创建并行队列
    concurrentQueue = dispatch_queue_create("com.atx610.NewMTX.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    // 获取主队列
    mainQueue = dispatch_get_main_queue();
    // 获取全局队列
    globalQueue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
    
    dispatch_async(serialQueue, ^{
        NSLog(@"异步,串行 == %@", [NSThread currentThread]);
    });
    dispatch_async(serialQueue, ^{
        NSLog(@"异步,串行 == %@", [NSThread currentThread]);
    });
    dispatch_async(serialQueue, ^{
        NSLog(@"异步,串行 == %@", [NSThread currentThread]);
    });
    
    dispatch_sync(serialQueue, ^{
        NSLog(@"同步,串行 == %@", [NSThread currentThread]);
    });
    dispatch_sync(serialQueue, ^{
        NSLog(@"同步,串行 == %@", [NSThread currentThread]);
    });
    dispatch_sync(serialQueue, ^{
        NSLog(@"同步,串行 == %@", [NSThread currentThread]);
    });
3、调整队列的优先级

对于主队列和全局队列的优先级是一般是无法更改的,如果更改会发生不可预知的状况,但不一定会崩溃。一般调整只是针对使用dispatch_queue_creat ()函数创建的队列,且一般更改的是将要进行异步的并发的对列。默认生成的队列优先级为默认优先级,一般操作不立即生效,但是如果队列处于未激活状态则会立即生效。

  • 函数:void dispatch_set_target_queue(dispatch_object_t object, dispatch_queue_t _Nullable queue);,第一个参数是目的参数,即想要更改优先级的队列,第二个参数是目标参数也是队列,一般使用全局队列dispatch_get_global_queue (),即让第一个参数队列的优先级与第二个参数队列的优先级保持一致。
4、时间函数的应用

在GCD中时间一般使用dispatch_time_t类型来表示,系统提供了两个相对时间常量:

  • DISPATCH_TIME_NOW: 现在
  • DISPATCH_TIME_FOREVER:永远

(1)、创建相对时间:
dispatch_time_t dispatch_time(dispatch_time_t when, int64_t delta);函数相对于默认时钟创建新的dispatch_time_t或修改现有的dispatch_time_t

  • 第一个参数when: 是一个时间参照,可以使用自定义的dispatch_time_t对象,也可以使用通提供的时间常量,一般使用DISPATCH_TIME_NOW,如果传入0,那将使用mach_absolute_time()是一个CPU依赖函数,返回一个基于系统启动后的时钟"嘀嗒"数)的结果。
  • 第二个参数delta:指相对于第一个参数的时间增量,单位是ull 即纳秒数。1s(秒) = 1000 ms(毫秒)= 1000000 µm(微妙)= 1000000000 ns(纳秒),所以这里可以用系统提供的几个常量:
#define NSEC_PER_SEC 1000000000ull // 1s
#define NSEC_PER_MSEC 1000000ull // 1ms
#define USEC_PER_SEC 1000000ull // 1ms
#define NSEC_PER_USEC 1000ull // 1µm

//举例,表示距离现在10秒后时间点
dispatch_time_t newTime = dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC);

(2)、创建绝对时间:
dispatch_time_t dispatch_walltime(const struct timespec *_Nullable when, int64_t delta); 这一函数常用来创建基于一个绝对时间的绝对时间,例如想得到2018年8月8号8时8分8秒向后再添加8秒的时间。

  • 第一个参数when: 是一个结构体类型时间参数(如下:)。其中tv_sec为创建struct timeval时到Epoch的秒数,tv_usec为微秒数,即秒后面的零头。比如当前我敲代码时的tv_sec为6666666666,tv_usec为666666,即当前时间距Epoch时间6666666666秒,666666微秒。
#ifndef _STRUCT_TIMESPEC
#define _STRUCT_TIMESPEC    struct timespec
_STRUCT_TIMESPEC
{
    __darwin_time_t tv_sec;
    long            tv_nsec;
};
#endif 
  • 第二个参数delta:与上面创建相对时间第二个参数一样。

使用:

    NSString * dateString = @"2018-08-08 08:08:08";
    NSDateFormatter * formatDate = [[NSDateFormatter alloc] init];
    formatDate.dateFormat = @"yyyy-MM-dd HH:mm:ss";
    NSDate * yearDate = [formatDate dateFromString:dateString];
    NSTimeInterval interval = [yearDate timeIntervalSince1970];
    
    double second, subSecond;
    struct timespec time;
    // 将浮点数分解为整数和小数
    subSecond = modf(interval, &second);
    time.tv_sec = second;
    time.tv_nsec = subSecond * NSEC_PER_SEC;
    dispatch_time_t absoluteTime = dispatch_walltime(&time, 8 * NSEC_PER_SEC);
// 这样就获得了一个相对于具体时间的绝对时间

(3)、延迟函数:
iOS中做一个延迟一段时间的方法有很多,其中在GDC中可以使用void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);函数来做延迟操作。

首先来举个🌰:

    dispatch_time_t newTime = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);
    dispatch_after(newTime, dispatch_get_main_queue(), ^{
        NSLog(@"三秒后的我~");
    });
  • 在这里需要注意:dispatch_after函数不是在指定时间后执行某种处理,而只是在指定时间将某种处理追加到Dispatch Queue中,但是不一定是立即执行该处理!

  • 第一个参数:when:指定时间,可以直接使用dispatch_time()或者dispatch_walltime()函数做参数。第二个参数:queue:指定要将处理追加到那个队列中。第三个参数:block:要追加的处理。

  • 如上面的例子所示,三秒后将追加处理加入主队列中,因为Main Dispatch Queue在主线程中的RunLoop中执行,所以在每隔1/60秒执行一次的RunLoop中,Block最快会在三秒后执行,最慢会在3 + 1/60秒后执行,但是其他线程不一定。

5、调度组 -- Dispatch groups

Dispatch groups是在一个或多个任务执行完成之前阻止线程的一种方式。类似于将所有任务加入到串行队列中,当最后一个任务执行完毕后再做下一步操作,但是不同的是Dispatch groups一般针对异步并发队列,因为你无法检测几个任务谁是最后执行完的,但是这几个任务不全部完成就无法进行下一步操作,或者下一步操作出现未知状况,Dispatch groups就是解决这样的问题的。

  • 使用dispatch_group_t表示组类型,其创建函数:dispatch_group_t dispatch_group_create(void);

  • 使用调度组进行异步操作时使用dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);函数来代替dispatch_async()函数。

  • long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);:直到与组关联的所有任务块都已完成 或 指定的时间已经超过时返回。成功返回0,在即指定时间内完成了组相关的所有任务,错误为非0,即超时。

  • void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);:该函数与上面最大的不同就是会一直等到组关联的所有任务块都已完成然后在会将本函数将执行的Block追加到Dispatch Queue中!

以上两个方法是配合dispatch_group_async ()一起使用的


  • void dispatch_group_enter(dispatch_group_t group);:手动指示Block已进入Dispatch Queue。调用此函数表示另一个Block通过dispatch_async()的方法加入了组。 调用此函数必须与dispatch_group_leave()配合使用。

  • void dispatch_group_leave(dispatch_group_t group);:手动指示Dispatch Queue中的Block已经完成。调用此函数表示Block已完成,并通过除dispatch_group_async()之外的方法离开调度组。

示例:

    dispatch_group_t newGroup = dispatch_group_create();
    dispatch_queue_t queue_change = dispatch_queue_create("com.atx610.NewMTX.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_async(newGroup, queue_change, ^{
        sleep(1);
        NSLog(@"test_1");
    });
    dispatch_group_async(newGroup, queue_change, ^{
        NSLog(@"test_2");
    });

    dispatch_group_enter(newGroup);
    dispatch_async(queue_change, ^{
        NSLog(@"test_3");
        dispatch_group_leave(newGroup);
    });
    
    dispatch_group_enter(newGroup);
    dispatch_async(queue_change, ^{
        sleep(2);
        NSLog(@"test_4");
        dispatch_group_leave(newGroup);
    });

    dispatch_group_notify(newGroup, queue_change, ^{
        NSLog(@"done,and start new work");
    });

//    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);
//    long result = dispatch_group_wait(newGroup, time);
//    if (result == 0) {
//        NSLog(@"在设置的时间内,组中的任务全部完成");
//    }else{
//        NSLog(@"在设置的时间内,组中的任务未全部完成");
//    }

结果:

test_2
test_3
test_1
test_4
done,and start new work
6、调度障碍 -- Dispatch Barrier

调度障碍API是一种向dispatch queue提交障碍Block的机制,类似于dispatch_async()/ dispatch_sync()API。主要用于在访问数据库或者文件时,实现高效的读/写方案。障碍Block 仅在提交到使用DISPATCH_QUEUE_CONCURRENT属性 创建的 队列(并发队列)时才有特殊的作用; 在个队列上,只有在障碍Block前添加到队列中的所有任务都已经完成,障碍Block才会运行,而在障碍Block完成前,障碍Block之后提交到队列的任何任务都将不会运行,直到障碍Block完成。
 需要特别注意的是:当提交到全局队列(global queue)或 未使用 DISPATCH_QUEUE_CONCURRENT属性创建的队列时,障碍Block与dispatch_async()/ dispatch_sync()API提交的Block的行为相同!!!

  • void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block); //异步执行障碍Block
  • void dispatch_barrier_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);//同步执行障碍Block

示例:

    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.atx610.NewMTX.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
//    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(concurrentQueue, ^{
        NSLog(@"test_1");
    });
    dispatch_async(concurrentQueue, ^{
        NSLog(@"test_2");
        sleep(2);
        NSLog(@"睡醒了~");
    });
    
    dispatch_barrier_async(concurrentQueue, ^{
        NSLog(@"I Am Barrier!");
    });
    
    dispatch_async(concurrentQueue, ^{
        NSLog(@"test_3");
    });
    dispatch_async(concurrentQueue, ^{
        NSLog(@"test_4");
    });

使用create函数创建的队列运行结果如下:

test_1
test_2
睡醒了~
I Am Barrier!
test_3
test_4

使用全局队列运行结果如下:

test_1
test_2
I Am Barrier!
test_3
test_4
睡醒了~
7、与执行次数相关的函数:

(1)、dispatch_apply 函数:
 该函数功能:将一个任务提交给调度队列进行多次调用。此函数在返回之前等待任务块完成,即 会将指定次数的指定Block追加到指定的队列中并等待全部处理执行结束! 如果目标队列是并发的,则该块可能会同时被调用。

  • void dispatch_apply(size_t iterations, dispatch_queue_t queue, DISPATCH_NOESCAPE void (^block)(size_t));:第一个参数 iterations表示重复次数,第三个参数block中有一个参数,表示该block按照重复次数添加到queue中时是第几个添加的。

常见使用方式:

    dispatch_queue_t globalQueue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
    dispatch_async(globalQueue, ^{
        dispatch_apply(5, globalQueue, ^(size_t index) {
            // 在次并列处理某些事件
            NSLog(@"%zu", index);
        });
        NSLog(@"apply循环 结束");
        // 只有apply中的事件全部处理完毕了才会进行后面的操作
        dispatch_async(dispatch_get_main_queue(), ^{
            // 主队列更新界面等操作
            NSLog(@"主队列更新");
        });
    });
0
1
2
3
4
apply循环 结束
主队列更新

(2)、dispatch_once 函数:
 dispatch_once_t 类型来定义一个变量,dispatch_once 函数来保证在应用程序中只执行一次指定处理的任务。

  • void dispatch_once(dispatch_once_t *predicate, DISPATCH_NOESCAPE dispatch_block_t block); :第一个参数是指向dispatch_once_t的指针,用于测试块是否已完成。其主要用于实现单例模式:
+ (instancetype)shareManager {
    static dispatch_once_t onceToken;
    static Manager * manager;
    dispatch_once(&onceToken, ^{
        manger = [[self alloc] init];
    });
    return manager;
}
8、调度信号量 -- Dispatch Semaphore

调度信号量用dispatch_semaphore_t表示,调度信号量与传统信号量相似,但通常效率更高。 调度信号量只有当因为信号量不可用而线程需要被阻塞时才调用到内核。 如果信号量可用,则不进行内核调用。

  • dispatch_semaphore_t dispatch_semaphore_create(long value);创建具有初始值的新计数信号量,传递小于零的值将导致返回NULL。

  • long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);;作用:减少计数信号量,将信号量减1.第二个参数是超时时间。成功返回零,如果发生超时则返回非零值。如果在时间内,当信号总量少于等于0的时候,这个函数就阻塞当前线程等待timeout,如果等待的期间信号量值被dispatch_semaphore_signal函数加1了,即该函数所处线程获得了信号量,那么就继续向下执行并将信号量减1。如果等待期间没有获取到信号量或者信号量的值一直为0,那么等到timeout时,其所处线程自动执行其后语句。

  • long dispatch_semaphore_signal(dispatch_semaphore_t dsema);:增加计数信号量, 如果以前的值等于零,此函数会在返回之前唤醒等待的线程。如果线程被唤醒,此函数返回非零值。 否则返回零。当返回值为0时表示当前并没有线程等待其处理的信号量,其处理的信号量的值加1即可。当返回值不为0时,表示其当前有一个或多个线程等待其处理的信号量,并且该函数唤醒了一个等待的线程(当线程有优先级时,唤醒优先级最高的线程;否则随机唤醒)。

使用:

    dispatch_queue_t global = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semap = dispatch_semaphore_create(0);
    dispatch_async(global, ^{
        dispatch_semaphore_wait(semap, DISPATCH_TIME_FOREVER);
        NSLog(@"first");
    });
    NSLog(@"second");
    dispatch_semaphore_signal(semap);

结果:

second
first
9、其他常见使用GCD函数
  • void dispatch_suspend(dispatch_object_t object);:暂停在调度对象上的剩余任务。这会增加内部暂停计数,调用dispatch_suspend()必须与调用dispatch_resume()进行平衡。

  • void dispatch_resume(dispatch_object_t object);: 是dispatch_suspend()的反向操作,并且消耗暂停计数。当最后一个暂停计数被消耗时,即暂停计数为零时,与该对象关联的任务将再次被调用。如果指定的对象具有零暂停计数,并且不是非活动的源,则此函数将导致断言并且进程终止,即Crash。

三、NSOperation / NSOperationQueue:

操作队列是一种面向对象的方法来封装要异步执行的工作,显示如何使用Objective-C对象来封装和执行任务。 NSOperation本身是抽象类,只能使用其子类,另外Foundation框架提供了两个具体的子类:NSBlockOperationNSInvocationOperation,除此之外还可以自定义继承自NSOperation的类并实现内部相应的方法来使用NSOperation。一般NSOperation和NSOperationQueue结合使用实现多线程并发。

1、关于抽象类NSOperation:

  • - (void)start; : 开启任务操作。NSOperation对象默认按同步方式执行,也就是在调用start方法的那个线程中直接执行。

  • - (void)main;: 类的主函数,一般在自己定义继承自NSOperation的类中重写该方法以实现执行任务。

  • @property (readonly, getter=isCancelled) BOOL cancelled; : 操作是否已经取消。

  • - (void)cancel;:operation开始执行之后, 默认会一直执行操作直到完成,我们也可以调用cancel方法中途取消操作。

  • @property (readonly, getter=isExecuting) BOOL executing;:操作是否正在执行。

  • @property (readonly, getter=isFinished) BOOL finished;:操作是否已经完成。

  • @property (readonly, getter=isAsynchronous) BOOL asynchronous;:是否是异步的。

  • @property (readonly, getter=isReady) BOOL ready;:是否准备就绪。线程start后并不是立即执行,而是进入一个就绪的状态(isReady),由系统调度执行。

  • - (void)addDependency:(NSOperation *)op; :添加依赖关系。

  • - (void)removeDependency:(NSOperation *)op;:移除依赖关系。

  • @property (readonly, copy) NSArray<NSOperation *> *dependencies;依赖关系数组。

  • @property NSOperationQueuePriority queuePriority;:优先级,具体如下:

typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};

-@property (nullable, copy) void (^completionBlock)(void);:任务操作完成后的Block

  • - (void)waitUntilFinished;:阻塞当前线程直到此任务执行完毕。

  • @property double threadPriority;:线程的优先级

  • @property NSQualityOfService qualityOfService;:服务质量,具体如下:

typedef NS_ENUM(NSInteger, NSQualityOfService) {
    NSQualityOfServiceUserInteractive = 0x21,
    NSQualityOfServiceUserInitiated = 0x19,
    NSQualityOfServiceUtility = 0x11,
    NSQualityOfServiceBackground = 0x09,
    NSQualityOfServiceDefault = -1
} NS_ENUM_AVAILABLE(10_10, 8_0);
  • @property (nullable, copy) NSString *name ;:给操作起个名字,做标记。

2、创建NSInvocationOperation对象:
 主要是使用- (nullable instancetype)initWithTarget:(id)target selector:(SEL)sel object:(nullable id)arg;这一方法,在你指定的对象上调用你指定的方法,还可以传递一个参数。如下所示:

NSString * name = @"lucy";
NSInvocationOperation * invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(logSomethingWithString:) object:name];
[invocationOperation start];
--------------
- (void)logSomethingWithString:(NSString *)name
{
    NSLog(@"%@", name);
}

3、创建NSBlockOperation对象:
 相对而言NSBlockOperation要比NSInvocationOperation好用一点,该类有两个方法和一个属性:

  • + (instancetype)blockOperationWithBlock:(void (^)(void))block;:创建并添加任务。
  • - (void)addExecutionBlock:(void (^)(void))block;:添加并发任务。
  • @property (readonly, copy) NSArray<void (^)(void)> *executionBlocks;:获取并发的block。

示例:

    NSBlockOperation * blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"001");
    }];
    [blockOperation addExecutionBlock:^{
        NSLog(@"002");
    }];
    [blockOperation addExecutionBlock:^{
        NSLog(@"003");
    }];
    [blockOperation addExecutionBlock:^{
        NSLog(@"004");
    }];
    [blockOperation start];
    NSLog(@"%@", blockOperation.executionBlocks);

4、NSOperationQueue -- 操作队列:
 操作队列提供一个异步执行的线程,同NSOperation配合使用可以高效利用管理操作和线程。

  • - (void)addOperation:(NSOperation *)op;:添加任务。
  • - (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait;:添加一组任务,并设置是否阻塞当前线程知道操作完成。
  • - (void)addOperationWithBlock:(void (^)(void))block ;:添加一个block形式的operation。
  • @property (readonly, copy) NSArray<__kindof NSOperation *> *operations;:获取队列中的操作。
  • @property (readonly) NSUInteger operationCount ;:获取队列中的操作的数量。
  • @property NSInteger maxConcurrentOperationCount;:设置、获取最大并发数量。设置完后如果任务数量超过了最大并发数量,会等待某一任务完成后再执行。
  • @property (getter=isSuspended) BOOL suspended;:队列是否已经暂停。当设置为YES后,它会暂停在此之后添加到队列中或正在运行任务之后的任务,在此之前添加的或者正在运行的任务不会受到影响。继续已暂停的队列只需将该属性设为NO即可。
  • @property (nullable, copy) NSString *name;:队列标识。
  • @property NSQualityOfService qualityOfService;:服务质量。
  • @property (nullable, assign) dispatch_queue_t underlyingQueue;:潜在队列,此属性的默认值为nil。可以将此属性的值设置为现有的某一队列,以使本排队的操作任务可以散布提交给该调度队列中执行。需要注意:只有当前队列中没有任何操作任务时,才能设置此属性的值; 如果operationCount不等于0时设置此属性的值会引发NSInvalidArgumentException。 该属性的值不能是dispatch_get_main_queue返回的值。 为潜在队列设置的服务质量级别将覆盖当前队列的qualityOfService属性值。
  • - (void)cancelAllOperations;:取消所有操作。
  • - (void)waitUntilAllOperationsAreFinished;:阻塞当前线程,等待所有操作任务都完成。
  • @property (class, readonly, strong, nullable) NSOperationQueue *currentQueue:获取执行当前的NSOperation的NSOperationQueue列队。
  • @property (class, readonly, strong) NSOperationQueue *mainQueue:获取系统主线程的NSOperationQueue列队。

示例:

    - (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    imageViewArray = [NSMutableArray array];
    for (int i = 0; i < 3; i++) {
        UIImageView * imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100 + 100*i, 100, 100)];
        [imageViewArray addObject:imageView];
        [self.view addSubview:imageView];
    }
    NSBlockOperation * blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"001 --- 001, thread == %@", [NSThread currentThread]);
        [imageViewArray[0] setImage:[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1504690123016&di=00957249b318d00f04c80439063671e0&imgtype=0&src=http%3A%2F%2Finuyasha.manmankan.com%2FUploadFiles_6178%2F201004%2F20100427165757723.jpg"]]]];
    }];
    
    NSBlockOperation * blockOperation_copy = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"copy --- 001, thread == %@", [NSThread currentThread]);
        [imageViewArray[1] setImage:[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1504690048974&di=ddc4518682a2e884cd9e88bbf7d9aa00&imgtype=0&src=http%3A%2F%2Fimg.qqu.cc%2Fuploads%2Fallimg%2F150601%2F1-150601201258.jpg"]]]];
    }];
    
    NSBlockOperation * blockOperation_black = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"black --- 001, thread == %@", [NSThread currentThread]);
        [imageViewArray[2] setImage:[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1504689993435&di=c52ca4a5d5f46fc2552d3e97dd4eb241&imgtype=0&src=http%3A%2F%2Fp4.gexing.com%2Fshaitu%2F20120825%2F1336%2F5038647615f85.jpg"]]]];
    }];
    // 添加依赖关系
    [blockOperation addDependency:blockOperation_copy];
    [blockOperation_copy addDependency:blockOperation_black];
    queue = [[NSOperationQueue alloc] init];
    queue.maxConcurrentOperationCount = 1;
    [queue addOperations:@[blockOperation, blockOperation_copy, blockOperation_black] waitUntilFinished:NO];
    [queue setSuspended:YES];
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [queue setSuspended:NO];
}
效果图.gif
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容