GCD和NSOperation

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); // 之后
  • 并行队列 系统默认提供了四个全局可用的并行队列,其优先级不同,分别为
  1. DISPATCH_QUEUE_PRIORITY_HIGH
  2. DISPATCH_QUEUE_PRIORITY_DEFAULT
  3. DISPATCH_QUEUE_PRIORITY_LOW
  4. 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 高级用法,这里有相关解析)。

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