Cocoa 并发编程
iOS 中的多线程,是 Cocoa 框架下的多线程,通过 Cocoa 的封装,可以让我们更为方便的进行多线程编程。
在介绍 Cocoa 并发编程之前,我们先理清会提到的几个术语:
- 线程:在进程中可以用线程去执行一些主进程之外的代码。OS X 中线程的实现基于 POSIX 的 pthread API。
- 进程:也是我们通常意义上提到的进程,一个正在执行中的程序实体,可以产生多个线程。
- 任务:一个抽象的概念,用于表示一系列需要完成的工作。
Cocoa 中封装了 NSThread, NSOperation, GCD 三种多线程编程方式,他们各有所长。
- NSThread
NSThread 是一个控制线程执行的对象,通过它我们可以方便的得到一个线程并控制它。NSThread 的线程之间的并发控制,是需要我们自己来控制的,可以通过 NSCondition 实现。
它的缺点是需要自己维护线程的生命周期和线程的同步和互斥等,优点是轻量,灵活。
- NSOperation
NSOperation 是一个抽象类,它封装了线程的细节实现,不需要自己管理线程的生命周期和线程的同步和互斥等。只是需要关注自己的业务逻辑处理,需要和 NSOperationQueue 一起使用。使用 NSOperation 时,你可以很方便的设置线程之间的依赖关系。这在略微复杂的业务需求中尤为重要。
- GCD
GCD(Grand Central Dispatch) 是 Apple 开发的一个多核编程的解决方法。在 iOS4.0 开始之后才能使用。GCD 是一个可以替代 NSThread 的很高效和强大的技术。当实现简单的需求时,GCD 是一个不错的选择。
在现代 Objective-C 中,苹果已经不推荐使用 NSThread 来进行并发编程,而是推荐使用 GCD 和 NSOperation,具体的迁移文档参见 Migrating Away from Threads。
Grand Central Dispatch(GCD)
Grand Central Dispatch(GCD) 是苹果在 Mac OS X 10.6 以及 iOS 4.0 开始引入的一个高性能并发编程机制,底层实现的库名叫 libdispatch。
GCD 主要的功劳在于:
- 把底层的实现隐藏起来,提供了很简洁的面向“任务” 的编程接口,让程序员可以专注于代码的编写。
- GCD 底层实现仍然依赖于线程,但是使用 GCD 时完全不需要考虑下层线程的有关细节(创建任务比创建线程简单得多),GCD 会自动对任务进行调度,以尽可能地利用处理器资源。
概念:
Dispatch Queue:Dispatch Queue 顾名思义,是一个用于维护任务的队列,它可以接受任务(即可以将一个任务加入某个队列)然后在适当的时候执行队列中的任务。
- Dispatch Sources:Dispatch Source 允许我们把任务注册到系统事件上,例如 socket 和文件描述符,类似于 Linux 中 epoll 的作用
- Dispatch Groups:Dispatch Groups 可以让我们把一系列任务加到一个组里,组中的每一个任务都要等待整个组的所有任务都结束之后才结束,类似 pthread_join 的功能
- Dispatch Semaphores:这个更加顾名思义,就是大家都知道的信号量了,可以让我们实现更加复杂的并发控制,防止资源竞争
这些东西中最经常用到的是 Dispatch Queue。之前提到 Dispatch Queue 就是一个类似队列的数据结构,而且是 FIFO(First In, First Out)队列,因此任务开始执行的顺序,就是你把它们放到 queue 中的顺序。GCD 中的队列有下面三种:
- Serial (串行队列) 串行队列中任务会按照添加到 queue 中的顺序一个一个执行。串行队列在前一个任务执行之前,后一个任务是被阻塞的,可以利用这个特性来进行同步操作。
(我们可以创建多个串行队列,这些队列中的任务是串行执行的,但是这些队列本身可以并发执行。例如有四个串行队列,有可能同时有四个任务在并行执行,分别来自这四个队列。)
- Concurrent(并行队列) 并行队列,也叫 global dispatch queue,可以并发地执行多个任务,但是任务开始的顺序仍然是按照被添加到队列中的顺序。具体任务执行的线程和任务执行的并发数,都是由 GCD 进行管理的。
(在 iOS 5 之后,我们可以创建自己的并发队列。系统已经提供了四个全局可用的并发队列,后面会讲到。)
- Main Dispatch Queue(主队列) 主队列是一个全局可见的串行队列,其中的任务会在主线程中执行。主队列通过与应用程序的 runloop 交互,把任务安插到 runloop 当中执行。因为主队列比较特殊,其中的任务确定会在主线程中执行,通常主队列会被用作同步的作用。
获取队列
按照上面提到的三种队列,我们有对应的三种获取队列的方式:
- 串行队列 系统默认并不提供串行队列,需要我们手动创建:
dispatch_queue_t queue;
queue = dispatch_queue_create("com.example.MyQueue", NULL); // OS X 10.7 和 iOS 4.3 之前
queue = dispatch_queue_create("com.example.MyQueue", DISPATCH_QUEUE_SERIAL); // 之后
- 并行队列 系统默认提供了四个全局可用的并行队列,其优先级不同,分别为
- DISPATCH_QUEUE_PRIORITY_HIGH
- DISPATCH_QUEUE_PRIORITY_DEFAULT
- DISPATCH_QUEUE_PRIORITY_LOW
- DISPATCH_QUEUE_PRIORITY_BACKGROUND
优先级依次降低。优先级越高的队列中的任务会更早执行:
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
当然我们也可以创建自己的并行队列:
queue = dispatch_queue_create("com.example.MyQueue", DISPATCH_QUEUE_CONCURRENT);
不过一般情况下我们使用系统提供的 Default 优先级的 queue 就足够了。
- 主队列 主队列可以通过 dispatch_get_main_queue() 获取:
dispatch_async(dispatch_get_main_queue(), ^{
// Update the UI
[imageVIew setImage:image];
});
自己创建的队列与系统队列有什么不同?
事实上,我们自己创建的队列,最终会把任务分配到系统提供的主队列和四个全局的并行队列上,这种操作叫做 Target queues。
(具体来说,我们创建的串行队列的 target queue 就是系统的主队列,我们创建的并行队列的 target queue 默认是系统 default 优先级的全局并行队列。所有放在我们创建的队列中的任务,最终都会到 target queue 中完成真正的执行。)
通过我们自己创建的队列,以及 dispatch_set_target_queue 和 barrier 等操作,可以实现比较复杂的任务之间的同步。
通常情况下,对于串行队列,我们应该自己创建,对于并行队列,就直接使用系统提供的 Default 优先级的 queue。
创建的 Queue 需要释放吗?
在 iOS 6 系统把 dispatch queue 也纳入了 ARC 管理的范围,就不需要我们进行手动管理了。
iOS6 以上就需要使用 strong 或者 weak 来修饰,不然会报错:
@property (nonatomic, strong) dispatch_queue_t queue;
执行任务
NSOperation 和 NSOperationQueue
(NSOperation 本身是可以单独使用的,不过单独使用的话并不能体现出 NSOperation 的强大之处,通常还是使用 NSOperationQueue 来执行 NSOperation。)
NSOperation 是一个抽象类,我们需要继承它并且实现我们的子类。
在 NSOperationQueue 中运行
NSOperationQueue 是一个专门用于执行 NSOperation 的队列。
在 OS X 10.6 之后,把一个 NSOperation 放到 NSOperationQueue 中,queue 会忽略 isAsynchronous 变量,总是会把 operation 放到后台线程中执行。
这样不管 operation 是不是异步的,queue 的执行都是不会造成主线程的阻塞的。
使用 Queue 可以很方便地进行并发操作,并且帮我们完成大部分的监视 operation 是否完成的操作。
MyOperation *op = [[MyOperation alloc] init];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:op]; // add 完 operation 就立即启动了
[queue waitUntilAllOperationsAreFinished]; // 阻塞当前线程,直到所有的 operation 全都完成
NSLog(@"Main Function");
(像这样,我们可以添加各个各样的 operation 到 queue 中,只要这些 operation 都正确地重载了 isExecuting 和 isFinished,就可以正确地被并发执行。)
Dependency
NSOperation 可以通过 addDependency 来依赖于其他的 operation 完成,如果有很多复杂的 operation,我们可以形成它们之间的依赖关系图,来实现复杂的同步操作:
[updateUIOperation addDependency: workerOperation];
Cancellation
NSOperation 有如下几种的运行状态:
- Pending
- Ready
- Executing
- Finished
- Canceled
除 Finished 状态外,其他状态均可转换为 Canceled 状态。
当 NSOperation 支持了 cancel 操作时,NSOperationQueue 可以使用 cancelAllOperatoins 来对所有的 operation 执行 cancel 操作。
不过 cancel 的效果还是取决于 NSOperation 中代码是怎么写的。(比如 对于数据库的某些操作线程来说,cancel 可能会意味着 你需要把数据恢复到最原始的状态。)
maxConcurrentOperationCount
默认的最大并发 operation 数量是由系统当前的运行情况决定的(来源),我们也可以强制指定一个固定的并发数量。
Queue 的优先级
NSOperationQueue 可以使用 queuePriority 属性设置优先级,具体的优先级有下面几种:
typedef enum : NSInteger {
NSOperationQueuePriorityVeryLow = -8,
NSOperationQueuePriorityLow = -4,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
} NSOperationQueuePriority;
在 Queue 中优先级较高的会先执行。
-
注1:尽管系统会尽量使得优先级高的任务优先执行,不过并不能确保优先级高的任务一定会先于优先级低的任务执行,即优先级并不能保证任务的执行先后顺序。要先让一个任务先于另一个任务执行,需要使用设置dependency 来实现。
-
注2:同 NSOperation 一样,NSOperationQueue 也具有若干 QoS 选项可供选择。
GCD 与 NSOperation 的对比
这是面试中经常会问到的一点,这两个都很常用,也都很强大。对比它们可以从下面几个角度来说:
-
首先要明确一点,NSOperationQueue 是基于 GCD 的更高层的封装(从 OS X 10.10 开始可以通过设置 underlyingQueue 来把 operation 放到已有的 dispatch queue 中。)
-
从易用性角度,GCD 由于采用 C 风格的 API,在调用上比使用面向对象风格的 NSOperation 要简单一些。
-
从对任务的控制性来说,NSOperation 显著得好于 GCD,和 GCD 相比支持了 Cancel 操作(注:在 iOS8 中 GCD 引入了 dispatch_block_cancel 和 dispatch_block_testcancel,也可以支持 Cancel 操作了),支持任务之间的依赖关系,支持同一个队列中任务的优先级设置,同时还可以通过 KVO 来监控任务的执行情况。(这些通过 GCD 也可以实现,不过需要很多代码,使用 NSOperation 显得方便了很多。)
-
从第三方库的角度,知名的第三方库如 AFNetworking 和 SDWebImage 背后都是使用 NSOperation,也从另一方面说明对于需要复杂并发控制的需求,NSOperation 是更好的选择(当然也不是绝对的,例如知名的 Parse SDK 就完全没有使用 NSOperation,全部使用 GCD,其中涉及到大量的 GCD 高级用法,这里有相关解析)。
网友评论