OS X 和 iOS 提供了几种不同的 API 来支持并发编程。每一个 API 都具有不同的功能和使用限制,这使它们适合不同的任务。同时,这些 API 处在不同的抽象层级上。
苹果的移动和桌面操作系统中提供了相同的并发编程API,例如 pthread 、 NSThread 、GCD 、NSOperationQueue,以及NSRunLoop。实际上把 run loop 也列在其中是有点奇怪,因为它并不能实现真正的并行,不过因为它与并发编程有莫大的关系,因此值得我们进行一些深入了解。
开发者可以使用 POSIX Thread API(即pthread),或者 Objective-C 中提供的对该 API 的封装 NSThread,来创建自己的线程。直接使用 pthread比较复杂。NSThread 是 Objective-C 对 pthread 的一个封装。通过封装,在 Cocoa 环境中,可以让代码看起来更加亲切。例如,开发者可以利用 NSThread 的一个子类来定义一个线程,在这个子类的中封装需要在后台线程运行的代码。
不论使用 pthread 还是 NSThread 来直接对线程操作,都是相对糟糕的编程体验,这种方式并不适合我们以写出良好代码为目标的编码精神。直接使用线程可能会引发的一个问题是,如果你的代码和所基于的框架代码都创建自己的线程时,那么活动的线程数量有可能以指数级增长。这在大型工程中是一个常见问题。例如,在 8 核 CPU 中,你创建了 8 个线程来完全发挥 CPU 性能。然而在这些线程中你的代码所调用的框架代码也做了同样事情(因为它并不知道你已经创建的这些线程),这样会很快产生成成百上千的线程。代码的每个部分自身都没有问题,然而最后却还是导致了问题。使用线程并不是没有代价的,每个线程都会消耗一些内存和内核资源。
两个基于队列的并发编程 API :GCD 和 operation queue 。它们通过集中管理一个被大家协同使用的线程池,来解决上面遇到的问题。
为了让开发者更加容易的使用设备上的多核CPU,苹果在 OS X 10.6 和 iOS 4 中引入了 Grand Central Dispatch(GCD)。
通过 GCD,开发者不用再直接跟线程打交道了,只需要向队列中添加代码块即可,GCD 在后端管理着一个线程池。GCD 不仅决定着你的代码块将在哪个线程被执行,它还根据可用的系统资源对这些线程进行管理。这样可以将开发者从线程管理的工作中解放出来,通过集中的管理线程,来缓解大量线程被创建的问题。
GCD 带来的另一个重要改变是,作为开发者可以将工作考虑为一个队列,而不是一堆线程,这种并行的抽象模型更容易掌握和使用。
操作队列(operation queue)是由 GCD 提供的一个队列模型的 Cocoa 抽象。GCD 提供了更加底层的控制,而操作队列则在 GCD 之上实现了一些方便的功能,这些功能对于 app 的开发者来说通常是最好最安全的选择。
NSOperationQueue 有两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。在两种类型中,这些队列所处理的任务都使用 NSOperation 的子类来表述。
目前在 iOS 和 OS X 中有两套先进的同步 API 可供我们使用:操作队列和 GCD 。其中 GCD 是基于 C 的底层的 API ,而操作队列则是 GCD 实现的 Objective-C API。
操作队列提供了在 GCD 中不那么容易复制的有用特性。其中最重要的一个就是可以取消在任务处理队列中的任务。而且操作队列在管理操作间的依赖关系方面也容易一些。
另一面,GCD 给予你更多的控制权力以及操作队列中所不能使用的底层函数。
有关Operation Queue的详细内容请参见此处: 并发编程之Operation Queue http://blog.xcodev.com/archives/operation-queue-intro/
下面介绍下GCD的使用方法。
GCD 公开有 5 个不同的队列,归属于两大类,即Main Dispatch Queue(运行在主线程中的 main queue,通过dispatch_get_main_queue
函数获得),Global Dispatch Queue(3 个不同优先级的后台队列,以及一个优先级更低的后台队列(用于 I/O),它们通过dispatch_get_global_queue
获得)。
另外,开发者可以创建自定义队列:串行或者并行队列,它们通过dispatch_queue_create
函数获得。自定义队列非常强大,在自定义队列中被调度的所有 block 最终都将被放入到系统的全局队列中和线程池中。
使用不同优先级的若干个队列乍听起来非常直接,不过,我们强烈建议,在绝大多数情况下使用默认的优先级队列就可以了。如果执行的任务需要访问一些共享的资源,那么在不同优先级的队列中调度这些任务很快就会造成不可预期的行为。这样可能会引起程序的完全挂起,因为低优先级的任务阻塞了高优先级任务,使它不能被执行。
下面给出两个使用GCD的例子。
例一:适用于有需要长时间进行处理的任务,例如下载文件 、访问数据库等,同时保证界面在处理该任务时能够响应用户的操作。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSURL * url = [NSURL URLWithString:@"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg"];
NSData * data = [[NSData alloc]initWithContentsOfURL:url];
UIImage *image = [[UIImage alloc]initWithData:data];
if (data != nil) {
dispatch_async(dispatch_get_main_queue(), ^{ // 下载图片成功后到主线程队列中执行,调用主界面进行显示
self.imageView.image = image;
});
}
});
例二:并行处理for循环中的任务
dispatch_apply(count, dispatch_get_global_queue(0, 0), ^(size_t i){ // 执行第2行的代码count次
results[i] = do_work(data, i);
});
total = summarize(results, count); // 这一行的代码在前面代码中的n(=count)项任务都执行后才开始执行。
使用GCD进行编程,开发者要做的只是定义想执行的任务并追加到适当的Dispatch Queue中。其中,“定义想执行的任务”使用Block语法来完成。Dispatch Queue中的任务按照FIFO的顺序进行处理,也就是先进入的任务先处理。另外,Dispatch Queue分为串行和并行两种。
Dispatch Queue分为下面三种:
Serial Dispatch Queue
又称为private dispatch queues,要求等待正在执行的任务完成,再执行下一个。而换句话说,其实就是Serial Dispatch Queue只会创建一个线程来执行任务。Serial queue通常用于同步访问特定的资源或数据。当你创建多个Serial queue时,虽然它们各自是同步执行的,但Serial queue与Serial queue之间是并发执行的。
Concurrent Dispatch Queue
可以并发地执行多个任务,但是执行的任务完成的顺序是随机的。Concurrent Dispatch Queue中后面的任务可以不必等待正在执行的任务完成就可以开始执行,也就是同时可以执行多个任务。
对于Concurrent Dispatch Queue,OS X和iOS的XNU内核会基于Dispatch Queue中的任务数量、CPU核数和CPU负荷等当前系统状态来决定创建多少个线程和并行执行多少个任务。
Main Dispatch Queue
它是全局可用的serial queue,它是在应用程序主线程上执行任务的。
dispatch_queue_create 用于创建用户线程队列。可以创建Serial/Concurrent Dispatch Queue 两种队列,即串行与并行队列。
1. 创建Serial Dispatch Queue。
dispatch_queue_t serialQueue =
dispatch_queue_create("com.SerialQueue", NULL);
dispatch_queue_create第一个参数是串行队列标识,一般用反转域名的格式表示以防冲突;第二个参数是queue的类型,设为NULL时默认是DISPATCH_QUEUE_SERIAL,将创建串行队列,也可以将其设置为DISPATCH_QUEUE_CONCURRENT来创建自定义并行队列。
可以创建多个串行队列,串行队列也可以并行执行。决不能随意的大量生产Serial Dispatch Queue。
2. 创建Concurrent Dispatch Queue
dispatch_queue_t concurrentQueue =
dispatch_queue_create("com.ConcurrentQueue",
DISPATCH_QUEUE_CONCURRENT);
Concurrent Dispatch Queue创建多少都没有问题,因为Concurrent Dispatch Queue所使用的线程由系统的XNU内核高效管理,不会影响系统性能。
3. 内存管理 由dispatch_queue_create方法生成的Dispatch Queue并不能由ARC来自动管理内存。可以使用dispatch_release、dispatch_retain来手动管理(引用计数式)。
但在目前看来,所用的OSX-10.8 开启的ARC已经不需要再用dispatch_release()来做管理。
4:对于串行队列,每创建一个串行队列,系统就会对应创建一个线程,同时这些线程都是并行执行的,只是在串行队列中的任务是串行执行的。大量的创建串行队列会导致大量消耗内存,这是不可取的做法。串行队列的优势在于它是一个线程,所以在操作一个全局数据时候是线程安全的。当想并行执行而不发生数据竞争时候可以用并行队列操作。
Main Dispatch Queue是在主线程中执行任务的Dispatch Queue。因为主线程只有1个,所以Main Dispatch Queue是Serial Dispatch Queue。追加到Main Dispatch Queue中的任务将在主线程的RunLoop中执行。因为是在主线程中执行,所以应该只将用户界面更新等一些必须在主线程中执行的任务追加到Main Dispatch Queue中。
dispatch_queue_t dispatch_main_queue = dispatch_get_main_queue();
Global Dispatch Queue是所有应用程序都能使用的Concurrent Dispatch Queue,属于并行队列。一般情况下,可以不必通过dispatch_queue_create函数生成并行队列(Concurrent Dispatch Queue),而是使用系统预定义的并行队列,即全局队列(Global Concurrent Dispatch Queues)。Global Dispatch Queue有4个队列优先级,分别是:High、Default、Low、Background,对应的参数分别为DISPATCH_QUEUE_PRIORITY_HIGH,DISPATCH_QUEUE_PRIORITY_DEFAULT,DISPATCH_QUEUE_PRIORITY_LOW,DISPATCH_QUEUE_PRIORITY_BACKGROUND。
dispatch_queue_t dispatch_global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
1、dispatch_async
为了避免界面在处理耗时的操作时卡死,比如读取网络数据,IO,数据库读写等,我们会在另外一个线程中处理这些操作,然后通知主线程更新界面。
用GCD实现这个流程的操作比NSThread,NSOperation的方法都要简单。代码框架结构如下:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 耗时的操作
dispatch_async(dispatch_get_main_queue(), ^{
// 更新界面
});
});
2、dispatch_sync
dispatch_async中的任务是异步执行的,就是说dispatch_async添加任务到执行队列后会立刻返回,而不会等待任务执行完成。然而,必要的话,你也可以调用dispatch_sync来同步的执行一个任务:
dispatch_sync(aQueue, ^{
//Do some work;
});
dispatch_sync
会阻塞当前线程直到提交的任务完全执行完毕。
3、dispatch_group_async的使用
dispatch_group_async可以实现监听一组任务是否完成,完成后得到通知执行其他的操作。这个方法很有用,比如你执行三个下载任务,当三个任务都下载完成后你才通知界面说完成的了。下面是一段例子代码: 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, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"group1");
});
dispatch_group_async(group, queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"group2");
});
dispatch_group_async(group, queue, ^{
[NSThread sleepForTimeInterval:3];
NSLog(@"group3");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"updateUi");
});
dispatch_release(group);
打印结果:
2012-09-25 16:04:16.737 gcdTest[43328:11303] group1
2012-09-25 16:04:17.738 gcdTest[43328:12a1b] group2
2012-09-25 16:04:18.738 gcdTest[43328:13003] group3
2012-09-25 16:04:18.739 gcdTest[43328:f803] updateUi
通过上面结果中的时间先后可以看出,每个一秒打印一个,当第三个任务执行后,upadteUi被打印。
4、dispatch_barrier_async的使用
dispatch_barrier_async是在前面的任务执行结束后它才执行,而且它后面的任务等它执行完成之后才会执行。
需要注意的是:
dispatch_queue_t queue = dispatch_queue_create("com.example.unique.identifier", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"dispatch_async1");
});
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:4];
NSLog(@"dispatch_async2");
});
dispatch_barrier_async(queue, ^{
NSLog(@"dispatch_barrier_async");
[NSThread sleepForTimeInterval:4];
});
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:1];
NSLog(@"dispatch_async3");
});
打印结果:
2012-09-25 16:20:33.967 gcdTest[45547:11203] dispatch_async1
2012-09-25 16:20:35.967 gcdTest[45547:11303] dispatch_async2
2012-09-25 16:20:35.967 gcdTest[45547:11303] dispatch_barrier_async
2012-09-25 16:20:40.970 gcdTest[45547:11303] dispatch_async3
请注意执行的时间,可以看到执行的顺序如上所述。
5、dispatch_apply
执行某个代码片段N次。
dispatch_queue_t globalQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(5, globalQ, ^(size_t index) {
// 执行5次
});
参考:
Grand Central Dispatch (GCD) Reference https://developer.apple.com/library/ios/documentation/Performance/Reference/GCD_libdispatch_Ref/index.html
并发编程之GCD http://blog.xcodev.com/archives/gcd-intro/
并发编程:API 及挑战 http://objccn.io/issue-2-1/
GCD 学习(一)简介 http://www.cnblogs.com/zhidao-chen/p/3368822.html
iOS多线程编程之Grand Central Dispatch(GCD)介绍和使用 http://blog.csdn.net/totogo2010/article/details/8016129
Grand Central Dispatch http://en.wikipedia.org/wiki/Grand_Central_Dispatch
GCD使用经验与技巧浅谈 http://tutuge.me/2015/04/03/something-about-gcd/