在iOS多线程中我们知道NSOperationQueue操作队列可以直接使用addDependency函数设置操作之间的依赖关系实现线程同步,还可以使用setMaxConcurrentOperationCount函数直接设置最大并发数量。那么在GCD中又是如何实现线程同步和控制最大并发数量的呢?
事实上在之前的问题中我们已经提到了GCD实现线程同步的两种方法了,一种是组队列(dispatch_group_t
),另一种是dispatch_barrier_(a)sync
,都是等待前面的任务完成后再执行某个任务。除此之外另外一种实现线程同步的方法是信号量机制。
GCD实现线程同步的方法:
组队列(dispatch_group):
举一个例子:用户下载一个图片,图片很大,需要分成很多份进行下载,使用GCD应该如何实现?使用什么队列?
使用Dispatch Group追加block到Global Group Queue,这些block如果全部执行完毕,就会执行通过dispatch_group_notify
添加到主队列中的block,进行图片的合并处理。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, queue, ^{ /*加载图片1 */ }); dispatch_group_async(group, queue, ^{ /*加载图片2 */ }); dispatch_group_async(group, queue, ^{ /*加载图片3 */ }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 合并图片… … });
阻塞任务(dispatch_barrier):
通过dispatch_barrier_async
添加的操作会暂时阻塞当前队列,即等待前面的并发操作都完成后执行该阻塞操作,待其完成后后面的并发操作才可继续。可以将其比喻为一根霸道的独木桥,是并发队列中的一个并发障碍点,或者说中间瓶颈,临时阻塞并独占。注意dispatch_barrier_async只有在并发队列中才能起作用,在串行队列中队列本身就是独木桥,将失去其意义。
可见使用dispatch_barrier_async
可以实现类似dispatch_group_t
组调度的效果,同时主要的作用是避免数据竞争,高效访问数据。
/* 创建并发队列 */ dispatch_queue_t concurrentQueue = dispatch_queue_create("test.concurrent.queue", DISPATCH_QUEUE_CONCURRENT); /* 添加两个并发操作A和B,即A和B会并发执行 */ dispatch_async(concurrentQueue, ^(){ NSLog(@"OperationA"); }); dispatch_async(concurrentQueue, ^(){ NSLog(@"OperationB"); }); /* 添加barrier障碍操作,会等待前面的并发操作结束,并暂时阻塞后面的并发操作直到其完成 */ dispatch_barrier_async(concurrentQueue, ^(){ NSLog(@"OperationBarrier!"); }); /* 继续添加并发操作C和D,要等待barrier障碍操作结束才能开始 */ dispatch_async(concurrentQueue, ^(){ NSLog(@"OperationC"); }); dispatch_async(concurrentQueue, ^(){ NSLog(@"OperationD"); });
2017-04-04 12:25:02.344 SingleView[12818:3694480] OperationB 2017-04-04 12:25:02.344 SingleView[12818:3694482] OperationA 2017-04-04 12:25:02.345 SingleView[12818:3694482] OperationBarrier! 2017-04-04 12:25:02.345 SingleView[12818:3694482] OperationD 2017-04-04 12:25:02.345 SingleView[12818:3694480] OperationC
信号量机制(dispatch_semaphore):
信号量机制主要是通过设置有限的资源数量来控制线程的最大并发数量以及阻塞线程实现线程同步等。
GCD中使用信号量需要用到三个函数:
- dispatch_semaphore_create用来创建一个semaphore信号量并设置初始信号量的值;
- dispatch_semaphore_signal发送一个信号让信号量增加1(对应PV操作的V操作);
- dispatch_semaphore_wait等待信号使信号量减1(对应PV操作的P操作);
那么如何通过信号量来实现线程同步呢?下面介绍使用GCD信号量来实现任务间的依赖和最大并发任务数量的控制。
使用信号量实现任务2依赖于任务1,即任务2要等待任务1结束才开始执行:
方法很简单,创建信号量并初始化为0,让任务2执行前等待信号,实现对任务2的阻塞。然后在任务1完成后再发送信号,从而任务2获得信号开始执行。需要注意的是这里任务1和2都是异步提交的,如果没有信号量的阻塞,任务2是不会等待任务1的,实际上这里使用信号量实现了两个任务的同步。
/* 创建一个信号量 */ dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); /* 任务1 */ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ /* 耗时任务1 */ NSLog(@"任务1开始"); [NSThread sleepForTimeInterval:3]; NSLog(@"任务1结束"); /* 任务1结束,发送信号告诉任务2可以开始了 */ dispatch_semaphore_signal(semaphore); }); /* 任务2 */ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ /* 等待任务1结束获得信号量, 无限等待 */ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); /* 如果获得信号量则开始任务2 */ NSLog(@"任务2开始"); [NSThread sleepForTimeInterval:3]; NSLog(@"任务2结束"); }); [NSThread sleepForTimeInterval:10];
通过打印的时间可以看到任务2是在任务1结束后紧接着执行的:
2017-06-02 21:21:37.777156+0800 OC[6869:324518] 任务1开始 2017-06-02 21:21:40.782648+0800 OC[6869:324518] 任务1结束 2017-06-02 21:21:40.782829+0800 OC[6869:324519] 任务2开始 2017-06-02 21:21:43.788198+0800 OC[6869:324519] 任务2结束
通过信号量控制最大并发数量:
通过信号量控制最大并发数量的方法为:创建信号量并初始化信号量为想要控制的最大并发数量,例如想要保证最大并发数为5,则信号量初始化为5。然后在每个新任务执行前进行P操作,等待信号使信号量减1;每个任务结束后进行V操作,发送信号使信号量加1。这样即可保证信号量始终在5以内,当前最多也只有5个以内的任务在并发执行。
/* 创建一个信号量并初始化为5 */ dispatch_semaphore_t semaphore = dispatch_semaphore_create(5); /* 模拟1000个等待执行的任务,通过信号量控制最大并发任务数量为5 */ for (int i = 0; i < 1000; i++) { /* 任务i */ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ /* 耗时任务1,执行前等待信号使信号量减1 */ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"任务%d开始", i); [NSThread sleepForTimeInterval:10]; NSLog(@"任务%d结束", i); /* 任务i结束,发送信号释放一个资源 */ dispatch_semaphore_signal(semaphore); }); } [NSThread sleepForTimeInterval:1000]; 打印结果为每次开启五个并发任务 2017-06-02 21:45:27.409067+0800 OC[7234:336757] 任务1开始 2017-06-02 21:45:27.409069+0800 OC[7234:336758] 任务2开始 2017-06-02 21:45:27.409103+0800 OC[7234:336759] 任务3开始 2017-06-02 21:45:27.409268+0800 OC[7234:336761] 任务4开始 2017-06-02 21:45:27.409887+0800 OC[7234:336756] 任务0开始 2017-06-02 21:45:37.415217+0800 OC[7234:336757] 任务1结束 2017-06-02 21:45:37.415370+0800 OC[7234:336759] 任务3结束 2017-06-02 21:45:37.415217+0800 OC[7234:336761] 任务4结束 2017-06-02 21:45:37.415217+0800 OC[7234:336758] 任务2结束 2017-06-02 21:45:37.415442+0800 OC[7234:336756] 任务0结束 2017-06-02 21:45:37.415544+0800 OC[7234:336760] 任务5开始 2017-06-02 21:45:37.415548+0800 OC[7234:336762] 任务6开始 2017-06-02 21:45:37.415614+0800 OC[7234:336765] 任务9开始 2017-06-02 21:45:37.415620+0800 OC[7234:336764] 任务8开始 2017-06-02 21:45:37.415594+0800 OC[7234:336763] 任务7开始 ... ...