GCD浅析
1.关于GCD
Grand Central Dispatch是异步执行任务的技术之一。我们先看一个简单的示例:
1 - (void)doSomethingInBackground { 2 [self performSelectorInBackground:@selector(startWork) withObject:nil]; 3 } 4 5 - (void)startWork { 6 //长时间处理操作 7 NSLog(@"进行长时间处理"); 8 9 [self performSelectorOnMainThread:@selector(endWork) withObject:nil waitUntilDone:YES]; 10 } 11 12 - (void)endWork { 13 NSLog(@"完成工作并刷新页面"); 14 }
这个示例实现的功能很简单:在后台线程中执行长时间的处理,处理结束后,在主线程中使用处理结果。上面实现方式是使用NSObject类的performSelectorInBackground和performSelectorOnMainThread来实现的。下面我们用GCD的方式来实现同样功能,做个对比:
1 dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_CONCURRENT); 2 dispatch_async(queue, ^{ 3 NSLog(@"进行长时间处理"); 4 dispatch_async(dispatch_get_main_queue(), ^{ 5 NSLog(@"完成工作并刷新页面"); 6 }); 7 });
通过上面的比较,我们可以知道,GCD更加的简洁,同样的功能,代码量减少了差不多一半。
2.多线程编程
什么是线程?线程(英语:thread)是操作系统能够进行运算调度的最小单位(来自维基百科)。iOS应用程序在启动后,首先会将包含在应用程序中的CPU指令配置到内存中,CPU从应用程序指定的地址开始,一个一个去执行CPU指令,一个CPU一次只能执行一个指令。由于现在的物理CPU芯片实际上有64个(64核)CPU,如果1个CPU核虚拟为两个CPU核工作,那么使用多个CPU核同时工作就是很正常的事情了。这种存在多个CPU核工作的即为“多线程”。
在实际开发中,多线程编程是一种容易发生问题的编程技术。我们常见的有这么几种:
- 数据竞争:多个线程更新相同的资源会导致数据的不一致;
- 死锁:多个线程相互持续等待;
- 内存消耗高:每个线程都是有内存开销的,太多线程会大量的消耗内存。
多线程存在上面的这些问题,那么我们是不是应该弃用呢?答案是否定的,因为使用多线程编程,可以让我们在执行长时间的处理时,保证用户界面的即时响应。
3.GCD的API
3.1Dispatch Queue
分发队列主要有两类:
- Serial Dispatch Queue:等待现在执行任务处理结束;
- Concurrent Dispatch Queue:不等待现在执行处理结束;
听概念是不是感觉很抽象?没关系,下面我们来看看代码。
1 dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_SERIAL); 2 3 dispatch_async(queue, ^{ 4 NSLog(@"blk1"); 5 }); 6 7 dispatch_async(queue, ^{ 8 NSLog(@"blk2"); 9 }); 10 11 dispatch_async(queue, ^{ 12 NSLog(@"blk3"); 13 }); 14 15 dispatch_async(queue, ^{ 16 NSLog(@"blk4"); 17 }); 18 19 dispatch_async(queue, ^{ 20 NSLog(@"blk5"); 21 });
可以看到,上面我们使用的是Serial Dispatch Queue,运行效果会怎么样呢?
可以看到:使用Serial Dispatch Queue,因为要等待正在执行的任务结束,并且同事执行的处理数只能有一个,所以会依次打印blk1、blk2、blk3、blk4、blk5。
现在将上面示例的DISPATCH_QUEUE_SERIAL改为DISPATCH_QUEUE_CONCURRENT,结果会怎么样呢?
结论是:使用Concurrent Dispatch Queue时,因为不用等待正在执行的任务结束,可以并行执行多个任务,也就是使用多个线程同时执行,但并行执行任务的数量取决于系统当前的状态。上面并行执行的线程情况截图如下:
3.2dispatch_queue_create
上面介绍了dispatch queue,那么怎么创建分发队列呢?我们可以使用GCD的API函数。
1 dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_SERIAL);
dispatch_queue_create函数的第一个参数指定队列的名称,这个名称会在XCode调试中显示,也会显示在应用程序崩溃时所生成的CrashLog中。因此我们命名最好时取一些有意义的,以方便我们来定位问题和代码。例如下面的代码:
1 dispatch_queue_t queue1 = dispatch_queue_create("com.gcd.test1", DISPATCH_QUEUE_SERIAL); 2 dispatch_queue_t queue2 = dispatch_queue_create("com.gcd.test2", DISPATCH_QUEUE_SERIAL); 3 dispatch_queue_t queue3 = dispatch_queue_create("com.gcd.test3", DISPATCH_QUEUE_SERIAL); 4 dispatch_queue_t queue4 = dispatch_queue_create("com.gcd.test4", DISPATCH_QUEUE_SERIAL); 5 dispatch_queue_t queue5 = dispatch_queue_create("com.gcd.test5", DISPATCH_QUEUE_SERIAL); 6 7 dispatch_async(queue1, ^{ 8 NSLog(@"blk1"); 9 }); 10 11 dispatch_async(queue2, ^{ 12 NSArray *arr = @[@1, @2, @3]; 13 NSLog(@"%@", arr[4]); 14 }); 15 16 dispatch_async(queue3, ^{ 17 NSLog(@"blk3"); 18 }); 19 20 dispatch_async(queue4, ^{ 21 NSLog(@"blk4"); 22 }); 23 24 dispatch_async(queue5, ^{ 25 NSLog(@"blk5"); 26 });
从代码可以看到,queue2会崩溃,实际运行后的CrashLog如下:
dispatch_queue_create函数的第二个参数指定创建队列的类型。如果想创建Serial Dispatch Queue,那么指定类型为DISPATCH_QUEUE_SERIAL或NULL;如果想创建Concurrent Dispatch Queue,那么指定类型为DISPATCH_QUEUE_CONCURRENT。
【注意】:dispatch_queue_create函数可以创建任意个Dispatch Queue,当生成多个Serial Dispatch Queue时,各个Serial Dispatch Queue会并行执行,系统对于每一个Serial Dispatch Queue会生成并使用一个线程。如果生成大量的Serial Dispatch Queue,那么就会创建很多线程。过多使用线程,会消耗大量内存,降低系统的响应性能。
3.3Main Dispatch Queue/Global Dispatch Queue
上面说了用dispatch_queue_create函数生成Dispatch Queue,现在聊一下系统标准提供的几个Dispatch Queue。
名称 | Dispatch Queue类型 | 说明 |
Main Dispatch Queue | Serial Dispatch Queue | 主线程执行 |
Global Dispatch Queue(High Priority) | Concurrent Dispatch Queue | 执行优先级:高 |
Global Dispatch Queue(Default Priority) | Concurrent Dispatch Queue | 执行优先级:默认 |
Global Dispatch Queue(Low Priority) | Concurrent Dispatch Queue | 执行优先级:低 |
Global Dispatch Queue(Background Priority) | Concurrent Dispatch Queue | 执行优先级:后台 |
怎么创建?
1 dispatch_queue_t mainQueue = dispatch_get_main_queue(); 2 dispatch_queue_t highQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); 3 dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 4 dispatch_queue_t lowQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0); 5 dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
3.4dispatch_after
从这个函数名称,我们也能猜到它的大概作用:在xx时间后,再做某件事情。
NSLog(@"Before:%@", [NSDate date]); dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC); dispatch_after(time, dispatch_get_main_queue(), ^{ NSLog(@"After:%@", [NSDate date]); });
【注意】:dispatch_after函数并不是在指定时间后执行处理,而只是在指定时间后追加处理(也就是Block)到Dispatch Queue。
3.5Dispatch_Group
假设现在有个新的需求:需要在 Dispatch Queue中的多个任务完成之后,再做某件事情。当然大家会马上反应过来,用Serial Dispatch Queue即可。但如果应用场景是Concurrent Dispatch Queue或者多个Dispatch Queue呢?
这个时候就需要Dispatch Group了。
1 dispatch_queue_t queue1 = dispatch_queue_create("com.gcd.test1", DISPATCH_QUEUE_SERIAL); 2 dispatch_queue_t queue2 = dispatch_queue_create("com.gcd.test2", DISPATCH_QUEUE_SERIAL); 3 dispatch_queue_t queue3 = dispatch_queue_create("com.gcd.test3", DISPATCH_QUEUE_SERIAL); 4 dispatch_queue_t queue4 = dispatch_queue_create("com.gcd.test4", DISPATCH_QUEUE_SERIAL); 5 dispatch_queue_t queue5 = dispatch_queue_create("com.gcd.test5", DISPATCH_QUEUE_SERIAL); 6 7 dispatch_group_t group = dispatch_group_create(); 8 dispatch_group_async(group, queue1, ^{ 9 NSLog(@"blk1"); 10 }); 11 dispatch_group_async(group, queue2, ^{ 12 NSLog(@"blk2"); 13 }); 14 dispatch_group_async(group, queue3, ^{ 15 NSLog(@"blk3"); 16 }); 17 dispatch_group_async(group, queue4, ^{ 18 NSLog(@"blk4"); 19 }); 20 dispatch_group_async(group, queue5, ^{ 21 NSLog(@"blk5"); 22 }); 23 24 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ 25 NSLog(@"done"); 26 });
多次运行,我们发现打印done总是在最后:
3.6dispatch_barrier_async
闭上眼睛,来想象这么一个场景:假设我们有一个本地数据库,会频繁的去读取数据和写入数据;写入是一个特殊的操作,如果在写入的时候有其他的读取操作,那么很有可能会读到脏数据,甚至也可能会造成程序的崩溃;如果只是单纯的读取操作,那么多个并行就不会发生问题。
针对这种场景,我们该怎么办呢?
1 dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_CONCURRENT); 2 3 dispatch_async(queue, ^{ 4 NSLog(@"blk1"); 5 }); 6 dispatch_async(queue, ^{ 7 NSLog(@"blk2"); 8 }); 9 dispatch_async(queue, ^{ 10 NSLog(@"blk3"); 11 }); 12 dispatch_barrier_async(queue, ^{ 13 NSLog(@"blk0"); 14 }); 15 dispatch_async(queue, ^{ 16 NSLog(@"blk4"); 17 }); 18 dispatch_async(queue, ^{ 19 NSLog(@"blk5"); 20 }); 21 dispatch_async(queue, ^{ 22 NSLog(@"blk6"); 23 });
多次运行看结果:
通过多次运行,我们发现,不管怎么变化,blk0的打印永远在blk1、blk2、blk3的后面,blk4、blk5、blk6的前面。
3.7dispatch_sync
前面一直在说的都是“async”,意思就是非同步的将Block追加到指定的Dispatch Queue,dispatch_async函数不做任何等待;
对应的就有“sync”,意思就是同步的将Block追加到指定的Dispatch Queue。在追加Block结束之前,dispatch_sync函数会一直等待。
1 dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_CONCURRENT); 2 dispatch_sync(queue, ^{ 3 NSLog(@"blk1"); 4 }); 5 dispatch_sync(queue, ^{ 6 NSLog(@"blk2"); 7 }); 8 dispatch_sync(queue, ^{ 9 NSLog(@"blk3"); 10 }); 11 dispatch_sync(queue, ^{ 12 NSLog(@"blk4"); 13 }); 14 dispatch_sync(queue, ^{ 15 NSLog(@"blk5"); 16 }); 17 18 NSLog(@"主线程");
多次运行看看结果,发现结果总是固定的,另外查看线程,执行都是在主线程。
如果使用dispatch_async呢?结果怎样?多次运行,可以发现顺序是随机的。
学到这里,我们已经接触到了这么4个概念:同步、异步、串行、并行。下面总结一下:
- 队列分为串行和并行,任务的执行分为同步和异步,异步是多线程的代名词,异步在实际应用中会开启新的线程,执行耗时操作;
- 队列只是负责任务的调度,而不负责任务的执行,任务是在线程中执行。
- 同步意味着在当前线程中执行,异步意味着会开启新的线程。
下面我们继续来细细品味一下这4个概念:串行同步、串行异步、并行同步、并行异步。
3.7.1串行同步
1 dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_SERIAL); 2 dispatch_sync(queue, ^{ 3 NSLog(@"blk1 thread:%@", [NSThread currentThread]); 4 }); 5 dispatch_sync(queue, ^{ 6 NSLog(@"blk2 thread:%@", [NSThread currentThread]); 7 }); 8 dispatch_sync(queue, ^{ 9 NSLog(@"blk3 thread:%@", [NSThread currentThread]); 10 }); 11 dispatch_sync(queue, ^{ 12 NSLog(@"blk4 thread:%@", [NSThread currentThread]); 13 }); 14 dispatch_sync(queue, ^{ 15 NSLog(@"blk5 thread:%@", [NSThread currentThread]); 16 }); 17 18 NSLog(@"主线程 thread:%@", [NSThread currentThread]);
运行结果:
结论:取出一个任务不放进别的线程,阻塞当前线程(不开辟新的线程,顺序执行),等任务执行完成,开始下一个任务。
3.7.2串行异步
1 dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_SERIAL); 2 dispatch_async(queue, ^{ 3 NSLog(@"blk1 thread:%@", [NSThread currentThread]); 4 }); 5 dispatch_async(queue, ^{ 6 NSLog(@"blk2 thread:%@", [NSThread currentThread]); 7 }); 8 dispatch_async(queue, ^{ 9 NSLog(@"blk3 thread:%@", [NSThread currentThread]); 10 }); 11 dispatch_async(queue, ^{ 12 NSLog(@"blk4 thread:%@", [NSThread currentThread]); 13 }); 14 dispatch_async(queue, ^{ 15 NSLog(@"blk5 thread:%@", [NSThread currentThread]); 16 }); 17 18 NSLog(@"主线程 thread:%@", [NSThread currentThread]);
运行结果:
结论:取出一个任务放进别的线程,不阻塞当前线程,等任务执行完成,开始下一个任务。
3.7.3并行同步
1 dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_CONCURRENT); 2 dispatch_sync(queue, ^{ 3 NSLog(@"blk1 thread:%@", [NSThread currentThread]); 4 }); 5 dispatch_sync(queue, ^{ 6 NSLog(@"blk2 thread:%@", [NSThread currentThread]); 7 }); 8 dispatch_sync(queue, ^{ 9 NSLog(@"blk3 thread:%@", [NSThread currentThread]); 10 }); 11 dispatch_sync(queue, ^{ 12 NSLog(@"blk4 thread:%@", [NSThread currentThread]); 13 }); 14 dispatch_sync(queue, ^{ 15 NSLog(@"blk5 thread:%@", [NSThread currentThread]); 16 }); 17 18 NSLog(@"主线程 thread:%@", [NSThread currentThread]);
运行结果:
结论:取出一个任务不放进别的线程,阻塞当前线程(不开辟新的线程,顺序执行),等任务执行完成,开始下一个任务。
3.7.4并行异步
dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ NSLog(@"blk1 thread:%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"blk2 thread:%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"blk3 thread:%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"blk4 thread:%@", [NSThread currentThread]); }); dispatch_async(queue, ^{ NSLog(@"blk5 thread:%@", [NSThread currentThread]); }); NSLog(@"主线程 thread:%@", [NSThread currentThread]);
运行结果:
结论:取出一个任务放进别的线程,不阻塞当前线程,不等任务执行完成,开始下一个任务(会开启多个线程)。
3.7.5总结
串行(Serial Dispatch Queue) | 并行(Concurrent Dispatch Queue) | 主线程(Main Queue) | |
同步(sync) |
没有开启新线程 串行执行任务 |
没有开启新线程 串行执行任务 |
没有开启新线程 串行执行任务 |
异步(async) |
开启一个新线程 串行执行任务 |
开启多个新线程 并行执行任务 |
没有开启新线程 串行执行任务 |
3.7.6死锁
死锁是同步(sync)线程操作中比较常见的一个问题,要理解死锁,我们要深刻的理解这么一句话:串行与并行说的是队列,队列是用来调度和存放任务的;而同步与异步,针对的是线程。区别在于,同步会阻塞当前线程,必须要等待同步线程中的任务执行完成以后,才能继续执行下一任务;而异步线程则是不用等待。下面我们来分析几个示例,更细的理解死锁是怎么来的,又是怎么没的。
示例1:
1 NSLog(@"任务1 thread:%@", [NSThread currentThread]); //任务1 2 dispatch_sync(dispatch_get_main_queue(), ^{ 3 NSLog(@"任务2 thread:%@", [NSThread currentThread]); //任务2 4 }); 5 NSLog(@"任务3 thread:%@", [NSThread currentThread]); // 任务3
结果:
发现后面的任务2和任务3都没有打印。
3.8dispatch_once
从函数名就知道,这个函数用于保证在应用程序执行中只执行一次指定处理。通常用于实现单例。
1 + (HDFNetConfig *)sharedConfig { 2 static id sharedInstance = nil; 3 static dispatch_once_t onceToken; 4 dispatch_once(&onceToken, ^{ 5 sharedInstance = [[self alloc] init]; 6 }); 7 return sharedInstance; 8 }
3.9dispatch_semaphore_t
当我们在处理一系列线程的时候,当线程数量达到一定量时,在以前我们会选择使用NSOperationQueue来处理并发控制,但如何在GCD中快速的控制并发呢?答案就是dispatch_semaphore。与它相关的共有三个函数,分别是dispatch_semaphore_create,dispatch_semaphore_signal,dispatch_semaphore_wait。
3.9.1dispatch_semaphore_create
该函数会创建一个信号量,定义如下:
dispatch_semaphore_t dispatch_semaphore_create(long value);
传入的参数为long,输出一个dispatch_semaphore_t类型且值为value的信号量。值得注意的是,这里的传入的参数value必须大于或等于0,否则dispatch_semaphore_create会返回NULL。
3.9.2dispatch_semaphore_signal
发送一个信号量,定义如下:
long dispatch_semaphore_signal(dispatch_semaphore_t dsema);
这个函数会使传入的信号量dsema的值加1。
返回值为long类型,当返回值为0时表示当前并没有线程等待其处理的信号量,其处理的信号量的值加1即可。当返回值不为0时,表示其当前有(一个或多个)线程等待其处理的信号量,并且该函数唤醒了一个等待的线程(当线程有优先级时,唤醒优先级最高的线程;否则随机唤醒)。
3.9.3dispatch_semaphore_wait
等待信号,定义如下:
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
这个函数会使传入的信号量dsema的值减1。
这个函数的作用是这样的:
- 如果dsema信号量的值大于0,该函数所处线程就继续执行下面的语句,并且将信号量的值减1;
- 如果desema的值为0,那么这个函数就阻塞当前线程等待timeout(注意timeout的类型为dispatch_time_t,不能直接传入整形或float型数),如果等待的期间desema的值被dispatch_semaphore_signal函数加1了,且该函数(即dispatch_semaphore_wait)所处线程获得了信号量,那么就继续向下执行并将信号量减1。如果等待期间没有获取到信号量或者信号量的值一直为0,那么等到timeout时,其所处线程自动执行其后语句。
-
当其返回值为0时表示在timeout之前,该函数所处的线程被成功唤醒。当其返回不为0时,表示timeout发生。
举个例子来说明一下信号量的工作原理:
停车场剩余4个车位,那么即使同时来了四辆车也能停的下。如果此时来了五辆车,那么就有一辆需要等待。
信号量的值就相当于剩余车位的数目,dispatch_semaphore_wait函数就相当于来了一辆车,dispatch_semaphore_signal就相当于走了一辆车。停车位的剩余数目在初始化的时候就已经指明了(dispatch_semaphore_create(long value)),调用一次dispatch_semaphore_signal,剩余的车位就增加一个;调用一次dispatch_semaphore_wait剩余车位就减少一个;当剩余车位为0时,再来车(即调用dispatch_semaphore_wait)就只能等待。有可能同时有几辆车等待一个停车位。有些车主没有耐心,给自己设定了一段等待时间,这段时间内等不到停车位就走了,如果等到了就开进去停车。而有些车主就像把车停在这,所以就一直等下去。
3.9.4示例
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2); dispatch_queue_t queue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_CONCURRENT); for (int i = 0; i < 100; i++) { dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); dispatch_async(queue, ^{ NSLog(@"%i %@",i, [NSThread currentThread]); sleep(2); dispatch_semaphore_signal(semaphore); }); }