引言
上一篇文章介绍了iOS中用于实现多线程的NSThread相关API,然而该框架需要手动管理线程的生命周期,使用起来很不方便。本篇将介绍iOS中最为出色的多线程框架GCD(Grand Central Dispatch)。事实上,GCD的作用远不止多线程操作,本篇将只介绍GCD较为常规的一些用法。
GCD是一个苹果公司用C语言开发的框架,好在iOS6.0以后,GCD对象被纳入了ARC的管理范围,因此不再需要手动管理GCD的内存了。如果你的工程部署的最低目标低于iOS6.0或者Mac OSX 10.8,则需要手动管理GCD内存。
不同于NSThread,GCD的操作重点放在了任务上,这使得编程者更能把精力集中在需要执行的操作上,而几乎不用在线程上花任何工夫。主要原因是GCD引入了两个非常重要的概念:Dispatch Queue。
任务 && 队列
任务(block)
任务即要执行的操作,在GCD中以block的形式提交给队列。因此在GCD中通常就用block来指代一个任务。
队列(Dispatch Queue)
队列全称调度队列(Dispatch Queue),是GCD中非常重要的一个抽象的概念,它用来存放任务,并安排线程来执行任务,因此在GCD中根本不需要手动管理线程。队列有两种:串行队列、并发队列
串行队列(Serial Dispatch Queue)
串行队列中的任务只能一个一个执行,根据FIFO的原则,执行完一个,再接着执行下一个。
并发队列(Concurrent Dispatch Queue)
并发队列中可以同时执行多个任务,系统会维护一个线程池来保证并发队列的执行。线程池会根据当前任务量自行安排线程的数量,以确保任务尽快执行。与串行队列相同的时,block的执行顺序也遵守FIFO规则,不同的是不用等上一个block执行完,下一个block就能开始执行。
队列获取
GCD提供了3中获取队列的方式:
- 主队列 是一特殊的串行队列,在应用程序创建的时候就和主线程绑定在一起了,其中的block只能在主线程执行。通过dispatch_get_main_queue()获取
-
全局并发队列 是系统共享的并发队列,有4种类型,对应4种优先级。
#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND无需创建,通过下面函数直接获取
dispatch_get_global_queue(long identifier, unsigned long flags);
第一个参数为优先级
第二个参数暂时无用,传0即可 -
自定义队列 通过下面函数创建
dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
第一个参数为队列的名称,调试的时候可以区分其他的队列,通常为反域名格式字符串;
第二个参数为队列属性,系统提供了两个宏定义分别用来创建串行队列和并发队列:DISPATCH_QUEUE_SERIAL(串行) DISPATCH_QUEUE_CONCURRENT(并发)。 若传NULL默认为DISPATCH_QUEUE_SERIAL
block提交方式
GCD提供了两种用于将block提交到指定的Dispatch Queue的方式,分别为同步方式和异步方式。
同步(synchronous)
所谓同步,即相关函数在将block提交给指定Dispatch Queue之后,不立即返回,而是必须等到block执行完毕才返回,否则将持续等待,在此期间线程阻塞。GCD中最直接的同步函数为
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
异步(asynchronous)
异步,和同步相对。函数在提交block之后不用等待block执行完毕就能直接返回,因此不会阻塞线程。GCD中最直接的异步函数为
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
组合方式
2种提交方式,3中队列类型,最终一个block的执行方式有6种可能
可以看出,只有通过异步函数提交的block才有可能并发执行。另外当用同步函数提交block到并发队列中时,即使并发队列有并发执行的能力,但是由于并没有开启新的线程,因此其中的block也执行串行执行。
需要注意的是,使用同步函数应当注意避免死锁,这在下面将会详细介绍。
dispatch_sync && dispatch_async
实际编码中,dispatch_async函数和dispatch_sync函数最常见的用法是异步函数嵌套同步函数,用于在并发中刷新UI。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
//TODO: 并发执行的代码
dispatch_sync(dispatch_get_main_queue(), ^{
//TODO: 主线程中刷新UI
});
});
事实上也不一定要用同步函数回调刷新UI,异步函数也可以,只要确保调度队列为主队列即可。实际应用中,很少会用到dispatch_sync,不仅因为dispatch_sync函数没有并发性,使用不当还容易造成死锁。
避免死锁(deadlock)
使用dispatch_sync函数不当,很容易造成死锁,卡死队列的执行线程。举个例子:
NSLog(@"111"); //任务1
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"222"); //任务2
});
NSLog(@"333"); //任务3
打印结果:只执行了任务1
执行过程分析:
首先,【任务1,同步函数,任务3】被装入主队列然后依次执行,当执行到同步函数时,提交任务2到主队列,同时主线程阻塞,等待任务2执行完毕。然而任务2是被添加到主队列的末位,在它前面的同步函数,任务3都没执行完毕,根本轮不到它执行。因此就形成了同步函数等任务2,任务2等任务3,任务3等同步函数的局面,即成死锁。
再看一段同样会造成死锁的代码
dispatch_queue_t queue = dispatch_queue_create("com.lotheve.gcd", DISPATCH_QUEUE_SERIAL);
NSLog(@"111"); //任务1
dispatch_async(queue, ^{
NSLog(@"222 %@",[NSThread currentThread]); //任务2
dispatch_sync(queue, ^{
NSLog(@"333 %@",[NSThread currentThread]); //任务3
});
NSLog(@"444 %@",[NSThread currentThread]); //任务4
});
NSLog(@"555"); //任务5
打印结果:任务3、4未执行
执行过程分析:
首先,【任务1,异步函数,任务5】被提交到主队列,任务1先执行。执行到异步函数时,提交了【任务2,同步函数,任务4】到一个自定义的串行队列中,同时异步函数直接返回,主线程继续执行主队列中剩余的任务。与此同时,系统创建一个新的线程来执行自定义队列中的任务。因此,任务2和任务5的执行顺序是不确定的。当新线程执行到同步函数时,同步函数提交任务3到自定义队列的末尾,并阻塞线程以等待任务3执行完毕。这时候就出现了和上一段代码一样的局面,形成死锁,读者可自行分析。
分析这两段代码的共同点发现:
- 执行同步函数的队列为串行队列
- 同步函数提交block的队列和执行同步函数的队列为同一个队列
事实上,只要以上两个条件均符合,必然会发生死锁。如果队列为并发队列,或者执行函数是异步函数,是肯定不会发生死锁的。
举个不会发生死锁的例子
dispatch_queue_t queue = dispatch_queue_create("com.lotheve.gcd", DISPATCH_QUEUE_SERIAL);
NSLog(@"222 %@",[NSThread currentThread]); //任务1
dispatch_sync(queue, ^{
NSLog(@"333 %@",[NSThread currentThread]); //任务2
});
NSLog(@"444 %@",[NSThread currentThread]); //任务3
分析过程:
主队列中依次执行【任务1,同步函数,任务2】,执行到同步函数的时候,提交任务2到自定义队列,主线程阻塞,等待任务2执行完毕。由于自定义队列中此时只有1个任务,因此主线程被重新调度(操作系统中线程是执行单元也是最小的调度单位),来执行任务2。任务2执行完毕后,主线程恢复主队列的执行上下文(复原CPU寄存器等信息,这些信息在阻塞之前保存在队列路径专用的内存块中),继续执行主队列中的任务,同步函数返回,任务3执行。
这段代码不满足“执行同步函数的队列和同步函数提交block的队列为同一个队列”,因此未符合死锁条件。
dispatch_set_target_queue
dispatch_queue_create创建的Dispatch Queue不管是Serial Dispatch Queue还是Concurrent Dispatch Queue,其执行优先级默认与默认优先级的Global Dispatch Queue执行优先级相同,即DISPATCH_QUEUE_PRIORITY_DEFAULT
。要想改变Dispatch Queue的执行优先级,就要使用dispatch_set_target_queue函数。
dispatch_queue_t serialQueue = dispatch_queue_create("com.lotheve.gcd", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
/*!
* 修改 serialQueue 的执行优先级,第一个参数为被修改队列,第二个参数为目标优先级队列
*/
dispatch_set_target_queue(serialQueue, globalQueue);
dispatch_set_target_queue函数除了用来修改Dispatch Queue的执行优先级,还可以用来设置Dispath Queue之间的层级结构。事实上,该函数是将源队列中的block转移到目标队列中执行。利用这一点可以将多个Dispatch Queue中的block转移到一个目标Dispatch Queue中,按照目标Dispatch Queue的执行方式执行。例如将多个Serial Dispatch Queue中的block转移到一个目标Serial Dispatch Queue中,使得这些block得以串行执行,否则各个Serial Dispatch Queue中的block将并发执行。
dispatch_queue_t targetQueue = dispatch_queue_create("com.lotheve.gcd", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue1 = dispatch_queue_create("com.lotheve.gcd", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("com.lotheve.gcd", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue3 = dispatch_queue_create("com.lotheve.gcd", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(queue1, targetQueue);
dispatch_set_target_queue(queue2, targetQueue);
dispatch_set_target_queue(queue3, targetQueue);
dispatch_async(queue1, ^{
NSLog(@"111: %@",[NSThread currentThread]);
});
dispatch_async(queue3, ^{
NSLog(@"333: %@",[NSThread currentThread]);
});
dispatch_async(queue2, ^{
NSLog(@"222: %@",[NSThread currentThread]);
});
执行结果:各block按照提交顺序顺序执行
dispatch_after
void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);
该函数用于延迟执行,需要注意的是并不是when时间后执行block,而是when时间后提交block到指定队列中,且是异步提交。
when:用于指定提交时间
queue:提交block的指定队列
block:执行的任务
This function waits until the specified time and then asynchronously adds block to the specified queue.
下面代码证明了dispatch_after函数是在若干时间后异步提交block到队列,而不是若干时间后执行block。
dispatch_queue_t queue = dispatch_queue_create("com.lotheve.gcd", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"111");
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), queue, ^{
NSLog(@"after 3 seconds %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"333");
});
执行结果:dispatch_after并没有阻塞线程,可见block后来才被提交。
Dispatch Group
现在有一种需求,要等所有block执行完毕后,进行最后的处理或发送通知。假如block只存在一个串行Serial Dispatch Queue中,那么在最后提交一个处理block就好了。但是如果block分布在不同的Dispatch Queue中,或者block是异步执行的,要想获知所有的block都执行完毕,显然要对所有的block进行监听。Dispatch Group就是用来实现这种需求的。
下面代码演示了多个并发执行的block全部执行完毕后在主线程做最后处理
//创建一个dispatch_group_t变量,然后将所有block提交到响应的queue并与group关联起来
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, ^{
NSLog(@"111~%@",[NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
NSLog(@"222~%@",[NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
[NSThread sleepForTimeInterval:3];
NSLog(@"333~%@",[NSThread currentThread]);
});
//当与group关联的所有block均执行完毕后,该函数向Main Queue提交了一个block
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"all done~%@",[NSThread currentThread]);
});
另外,dispatch_group_wait函数可以用来监听指定时间后的执行结果,但不能做结束处理。第一个参数为监听的group,第二个对象为dispatch_time_t变量,用于指定等待时间。
该函数在等待期间会阻塞当前线程,最终有2种可能结果:
- 指定时间内block执行完毕,立即返回,返回值为0。
- 指定时间内block未执行完毕,时间到了之后返回,返回值为1
需要注意的是这个函数不会影响group中block的执行,即使指定时间后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, ^{
NSLog(@"111~%@",[NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
NSLog(@"222~%@",[NSThread currentThread]);
});
dispatch_group_async(group, queue, ^{
[NSThread sleepForTimeInterval:3];
NSLog(@"333~%@",[NSThread currentThread]);
});
long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)));
if (result) {
NSLog(@"未执行完成");
}else{
NSLog(@"执行完毕");
}
//从这条打印语句最后执行可以看出等待期间当前线程阻塞
NSLog(@"done");
dispatch_barrier_async
在访问数据库或者文件时,允许多个用户同时读取,但读取期间不能有任何用户写入,写入期间也不能对其读取。dispatch_barrier_async函数可以方便地实现这种需求。实际操作中应该用信号量严格控制线程同步和资源互斥,这里只是对dispatch_barrier_async作简单介绍。
在一个自定义的Concurrent Dispatch Queue中执行读写操作,异步添加读操作,若在中间用dispatch_async追加一个写操作,很可能造成数据竞争的问题,因为读写线程可能同时访问了资源。若用dispatch_barrier_async来追加一个写操作,就不会出现这种问题。该函数的作用是在指定并发队列中异步追加一个barrier block,并标记一个点,当该队列中处在block前面的所有block执行完毕时才执行barrier block,执行期间其后面的block无法执行。当barrier block执行完毕,队列恢复正常,开始执行barrier block后面的block。
注意:提交block的队列必须是一个自定义Dispatch Concurrent Queue。若是串行队列或者全局并发队列,这个的作用等同于Dispatch_async。
The queue you specify should be a concurrent queue that you create yourself using the dispatch_queue_create function. If the queue you pass to this function is a serial queue or one of the global concurrent queues, this function behaves like the dispatch_async function.
dispatch_queue_t queue = dispatch_queue_create("com.lotheve.gcd", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^(){NSLog(@"111 %@",[NSThread currentThread]);});
dispatch_async(queue, ^(){NSLog(@"222");});
dispatch_async(queue, ^(){NSLog(@"333");});
dispatch_async(queue, ^(){[NSThread sleepForTimeInterval:2]; NSLog(@"444");});
dispatch_barrier_async(queue, ^{
NSLog(@"write");
});
dispatch_async(queue, ^(){NSLog(@"555");});
dispatch_async(queue, ^(){NSLog(@"666");});
dispatch_async(queue, ^(){NSLog(@"777");});
dispatch_async(queue, ^(){NSLog(@"888");});
执行结果:
另外还有个与之对应的dispatch_barrier_sync函数,区别在于该函数同步执行,即等到提交到队列的barrier block执行完毕才返回。而dispatch_barrier_async则是将barrier block提交后立即返回,但是也要等到队列中的该barrier block执行完毕后,其后面排队等待的block才会恢复正常并发执行。因此,使用该函数要避免因串行队列嵌套造成的死锁。另外作为优化,dispatch_barrier_sync提交的barrier block通常在当前线程执行。
dispatch_apply
dispatch_apply函数相当于dispatch_sync和dispatch_group的结合,其作用是按指定次数将指定的block提交到指定的Dispatch Queue中,等待全部的block执行完毕然后返回。从该描述上来看,该函数是同步的,有执行等待。因此功能上相当于dispatch_group外面套了一个dispatch_sync,或者说是特殊的dispatch_group_wait。
/*!
* dispatch_apply
*
* @param iterations 迭代次数
* @param queue 指定队列
* @param ^block 迭代block,附带了一个表示当前执行的block索引号的回调参数
*/
void dispatch_apply( size_t iterations, dispatch_queue_t queue, void (^block)( size_t));
若提交block的Dispatch Queue是串行队列,则各个block串行执行;若是并发队列,则并发执行。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index) {
NSLog(@"%ld %@",index,[NSThread currentThread]);
});
NSLog(@"all done!");
执行结果:可以看出该函数是同步执行的
dispatch_suspend && dispatch_resume
有时可能希望临时暂停已经提交到Dispatch Queue中的block执行,可以使用dispatch_suspend和dispatch_resume函数将queue挂起以及恢复。
调用dispatch_suspend后,并不是立即停止对应Dispatch Queue中的block执行,而是等当前正在执行的block执行完毕后才将队列挂起,后面的block得不到执行。
调用dispatch_resume后,继续执行队列中尚未执行的block。
dispatch_queue_t queue = dispatch_queue_create("com.lotheve.gcd", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"111"); //block1
});
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:3];
NSLog(@"222"); //block2
});
dispatch_async(queue, ^{
NSLog(@"333"); //block3
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"挂起");
dispatch_suspend(queue);
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"恢复");
dispatch_resume(queue);
});
执行结果:调用dispatch_suspend时,block2正在执行,因此block2执行完毕后,queue被挂起,block3得不到执行。随后调用dispatch_resume,queue恢复,block3执行。
dispatch_semaphore
在所有的多线程环境中都存在信号量,它的作用是用来保证线程同步或者资源互斥。在信号量上我们定义两种操作:wait(等信号) 和 sigbal(发信号)。
当一个线程调用Wait操作时,它要么得到资源然后将信号量减一,要么一直等下去(线程阻塞),直到信号量大于等于一时占有资源然后将信号量减一,解放线程,该操作用来标记占用资源;调用signal操作实际上是在信号量上执行加操作,该操作用来标记释放资源。
GCD提供了相应的dispatch_semaphore创建、wait、signal函数:
dispatch_semaphore_t dispatch_semaphore_create( long value);
创建一个信号量,参数为信号量计数初始值。
long dispatch_semaphore_wait( dispatch_semaphore_t dsema, dispatch_time_t timeout);
wait操作,第一个参数为信号量,第二个参数为等待时间。当指定时间内等信号量成功,返回0;否则超时返回1。
long dispatch_semaphore_signal( dispatch_semaphore_t dsema);
signal操作,参数为信号量。signal操作成功返回0,否则返回1。
dispatch_once
该函数用来保证一个block在整个应用程序生命周期中只执行一次,因此通常用该函数来实现单例。
下面代码用来创建一个Car的单例。
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
car = [[Car alloc]init];
});
return car;
dispatch_once_t必须是一个static类型变量,它用来标记block是否已经执行过。
总结
本篇介绍了GCD的几种常见的用法,可以看到通过GCD可以轻松实现多线程,而无需在线程本身上煞费苦心,并且ARC环境下也无需管理内存,使用起来非常方便。下一篇将介绍苹果的另一个多线程框架NSOperation,这是一个更上层的框架,对GCD进行了封装,主要基于队列使用,对于复杂的多线程项目使用起来比较方便。
参考文档:
《Grand Central Dispatch (GCD) Reference》
《小笨狼漫谈多线程:GCD(一)》
《iOS多线程编程Part 3/3 - GCD》
参考书籍:《Objective-C高级编程 iOS与OS X多线程和内存管理》