iOS开发:深入理解GCD 第一篇
最近把其他书籍都放下了,主要是在研究GCD。如果是为了工作,以我以前所学的GCD、NSOperation等知识已经足够用了,但学习并不仅仅知识满足于用它,要知其然、并且知其所以然,这样才可以不断的提高自身技术水平。
本文主要参考http://www.raywenderlich.com/60749/grand-central-dispatch-in-depth-part-1 和 《iOS与OS X 多线程和内存管理》,以及其他一些杂七杂八的书籍或者博客。
GCD已经面世很久了,基于GCD面向对象的多线程技术NSOperation也出现很久了,但并不是所有人都明了GCD主要内容,并发一直很棘手(虽然AFNetworking框架在一定程度上避免了很多时候我们在程序内赤裸裸的写多线程代码,但我想任何一个有想法的程序员都会深入理解并发编程,并将之掌握的),它们就像一组尖锐的棱角戳进 Objective-C 的平滑世界。
在这里,我将会四个篇幅来梳理自己所知、所理解的GCD。
第一、二篇主要是解释GCD是什么、能做什么,会提供我所编写好的一些代码片段,必要时,会有demo。
第三篇和第三篇主要是学习一些高级GCD提供的高级函数,如果时间充足,我会尽量以文字+代码+demo展示。
如果你对GCD和Block(代码块)完全陌生,请先看这篇文章:http://www.raywenderlich.com/4295/multithreading-and-grand-central-dispatch-on-ios-for-beginners-tutorial 《iOS上GCD和多线程的入门教程》
进程:也就是一个正在运行的应用程序。
线程:进程中的某一条完整的执行路径。一个进程可以有多个线程,至少有一个线程,即主线程。在iOS开发中,所有涉及UI界面的,必须在主线程中更新。
什么是GCD?
苹果官方给出的解释:GCD是异步执行任务的技术之一。一般将应用程序中记述的线程管理代码在系统集中实现,开发者只需要定义想执行的任务并追加到
适当的Dispatch Queue中,GCD就可以生成必要的线程并计划执行任务。
它具有以下优点:
- GCD可以将花费时间极其长的任务放到后台线程,可以改善应用的响应性能
- GCD 提供一个易于使用的并发模型而不仅仅只是锁和线程,以帮助我们避开并发陷阱
- GCD 具有在常见模式(例如单例)上用更高性能的原语优化你的代码的潜在能力(后面会提供一个单例的medo)
- 等等
如下面的代码片段:
dispatch_queue_t queue = dispatch_queue_create("cn.chutong.www", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ /** 放一些极其耗时间的任务在此执行 */ dispatch_async(dispatch_get_main_queue(), ^{ /** 耗时任务完成,拿到资源,更新UI 更新UI只可以在主线程中更新 */ }); });
我创建了一个并行队列queue,并异步执行耗时操作,当耗时操作执行完毕,我拿到其中的资源回到主线程来更新相应的UI,在这个Block代码块之外,主线程并不会被耗时任务所堵塞,可以流畅的处理其他事情。
GCD的一些术语
要理解 GCD ,要先熟悉与线程和并发相关的几个概念。
串行(Serial)与 并发(Concurrent)
任务串行,意味着在同一时间,有且只有一个任务被执行,即一个任务执行完毕之后再执行下一个任务。
任务并发,意味着在同一时间,有多个任务被执行。
同步(Synchronous)与 异步 (Asynchronous)
同步,意味着在当前线程中执行任务,不具备开启新的线程的能力。
异步,在新的线程中执行任务,具备开启新的线程的能力。
在 GCD 中,这些术语描述当一个函数相对于另一个任务完成,此任务是该函数要求 GCD 执行的。一个同步函数只在完成了它预定的任务后才返回。
一个异步函数,刚好相反,会立即返回,预定的任务会完成但不会等它完成。因此,一个异步函数不会阻塞当前线程去执行下一个函数。
临界区(Critical Section)
就是一段代码不能被并发执行,也就是,两个线程不能同时执行这段代码。这很常见,因为代码去操作一个共享资源,例如一个变量若能被并发进程访问,那么它很可能会变质(它的值不再可信)。
死锁(Deadlock)
停止等待事情的线程会导致多个线程相互维持等待,即死锁。
两个(有时更多)东西——在大多数情况下,是线程——所谓的死锁是指它们都卡住了,并等待对方完成或执行其它操作。第一个不能完成是因为它在等待第二个的完成。但第二个也不能完成,因为它在等待第一个的完成。
代码片段:
- (void)viewDidLoad { [super viewDidLoad]; dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"111111"); }); NSLog(@"222222"); }
执行上面的代码,你会发现没有任何打印,这个时候就是发生了死锁,我们禁止在主队列(iOS开发中,主队列是串行队列)中,在同步使用主队列执行任务,同理,禁止在同一个同步串行队列中,再使用该串行队列同步的执行任务,因为这样会造成死锁。
代码片段:
- (void)viewDidLoad { [super viewDidLoad]; dispatch_queue_t queue = dispatch_queue_create("cn.chutong.www", DISPATCH_QUEUE_SERIAL); dispatch_sync(queue, ^{ NSLog(@"111111"); dispatch_sync(queue, ^{ NSLog(@"22222"); }); NSLog(@"3333333"); }); NSLog(@"44444444"); }
会发现,只是打印了一次,然后就造成了死锁。
线程安全(Thread Safe)
线程安全的代码能在多线程或并发任务中被安全的调用,而不会导致任何问题(数据损坏,崩溃,等)。线程不安全的代码在某个时刻只能在一个上下文中运行。一个线程安全代码的例子是 NSDictionary 。你可以在同一时间在多个线程中使用它而不会有问题。另一方面,NSMutableDictionary 就不是线程安全的,应该保证一次只能有一个线程访问它。
上下文切换(Context Switch)
一个上下文切换指当你在单个进程里切换执行不同的线程时存储与恢复执行状态的过程。这个过程在编写多任务应用时很普遍,但会带来一些额外的开销。
并发与并行
并行要求并发,但并发不能保证并行,就计算机操作系统来说,开启线程是很耗性能的,也就是说,事实上,在某次并行处理任务中,开启的线程是有上限的,如果上限为2,即每次开启的新线程为2,那么是有可能出现并发却不并行的情况。
并发代码的不同部分可以“同步”执行。然而,该怎样发生或是否发生都取决于系统。多核设备通过并行来同时执行多个线程;然而,为了使单核设备也能实现这一点,它们必须先运行一个线程,执行一个上下文切换,然后运行另一个线程或进程。这通常发生地足够快以致给我们并发执行地错觉,如下图所示:
队列(Queue)
苹果官方对GCD的说明:开发者要做的只是定义想执行的任务并追加到适当的Dispatch Queue中。
这句话的源码如下:
dispatch_async(queue, ^{ /** * 想要执行的任务 */ });
该源码使用Block语法“定义想执行的任务”,通过dispatch_async函数“追加”赋值在变量queue的"Dispatch Queue中"。仅仅是这样,就可以使得指定的Block在另一线程中执行。
GCD 提供有 dispatch queue 来处理代码块,这些队列管理你提供给 GCD 的任务并用 FIFO (先进先出)顺序执行这些任务。这就保证了第一个被添加到队列里的任务会是队列中第一个开始的任务,而第二个被添加的任务将第二个开始,如此直到队列的终点。
Dispatch Queue是什么呢?是执行处理的等待队列,程序员通过dispatch_async等API,在Block语法中记述想要执行的处理,并将其追加到Dispatch Queue中。Dispatch Queue按照追加的顺序进行处理。
所有的调度队列(dispatch queue)自身都是线程安全的,你能从多个线程并行的访问它们。 GCD 的优点是显而易见的,即当你了解了调度队列如何为你自己代码的不同部分提供线程安全。关于这一点的关键是选择正确类型的调度队列和正确的调度函数来提交你的工作。
另外,在执行处理时,存在本文前面提到的两种Dispatch Queue,一种是等待现在执行中处理的Serial Dispatch Queue,另一种是Concurrent Dispatch Queue。
串行队列(Serial Dispatch Queue )
这些任务的执行时机受到 GCD 的控制;唯一能确保的事情是 GCD 一次只执行一个任务,并且按照我们添加到队列的顺序来执行。
如下代码,当调用serialPrintNumber方法时:
#import "ViewController.h" @interface ViewController () @property (nonatomic, strong) dispatch_queue_t serialQueue; @property (nonatomic, strong) dispatch_queue_t concurrentQueue; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.serialQueue = dispatch_queue_create("cn.chutong.www", DISPATCH_QUEUE_SERIAL); self.concurrentQueue = dispatch_queue_create("cn.chutong.www", DISPATCH_QUEUE_CONCURRENT); for (int i = 0; i < 100; i++) { [self serialPrintNumber:i]; } } /** * 异步串行队列 * */ - (void)serialPrintNumber:(int)number { dispatch_async(self.serialQueue, ^{ NSLog(@"%d %@",number, [NSThread currentThread]); }); } /** * 异步并行队列 * */ - (void)concurrentPrintNumber:(int)number { dispatch_async(self.concurrentQueue, ^{ NSLog(@"%d %@",number, [NSThread currentThread]); }); } @end
可以看到这样的打印:
开辟了一个新的子线程,任务是按顺序执行的。先进先出顺序执行的。因为要等待前一个任务处理结束,即同一时间,只能处理一个任务,才可以开始处理下一任务。
并发队列(Concurrent Dispatch Queue)
在并发队列中的任务能得到的保证是它们会按照被添加的顺序开始执行,但这就是全部的保证了。任务可能以任意顺序完成,你不会知道何时开始运行下一个任务,或者任意时刻有多少 Block 在运行。再说一遍,这完全取决于 GCD
代码片段:
#import "ViewController.h" @interface ViewController () @property (nonatomic, strong) dispatch_queue_t serialQueue; @property (nonatomic, strong) dispatch_queue_t concurrentQueue; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.serialQueue = dispatch_queue_create("cn.chutong.www", DISPATCH_QUEUE_SERIAL); self.concurrentQueue = dispatch_queue_create("cn.chutong.www", DISPATCH_QUEUE_CONCURRENT); for (int i = 0; i < 100; i++) { [self concurrentPrintNumber:i]; } } /** * 异步串行队列 * */ - (void)serialPrintNumber:(int)number { dispatch_async(self.serialQueue, ^{ NSLog(@"%d %@",number, [NSThread currentThread]); }); } /** * 异步并行队列 * */ - (void)concurrentPrintNumber:(int)number { dispatch_async(self.concurrentQueue, ^{ NSLog(@"%d %@",number, [NSThread currentThread]); }); } @end
打印:
可以看到,不再是顺序执行任务,开了多个线程。至此,我们可以清楚的知道,所谓“并行执行”,就是使用多个线程同时处理多个任务,因为完成任务所需要消耗的时间不同,所以完成任务的最终时间不同。
Serial Dispatch Queue 与 Concurrent Dispatch Queue 和线程之间的关系,如下图: