GCD
1、iOS中实现多线程的几种方式
在iOS中每个进程启动后都会建立一个主线程(UI线程),这个线程是其他线程的父线程。由于在iOS中除了主线程,其他子线程是独立于Cocoa Touch的,所以只有主线程可以更新UI界面。iOS中多线程使用并不复杂,关键是如何控制好各个线程的执行顺序、处理好资源竞争问题。
多线程的实现有以下几种方式:

1、什么是GCD
GCD(Grand Central Dispatch) 伟大的中央调度系统,是苹果为多核并行运算提出的C语言并发技术框架。GCD会自动利用更多的CPU内核;会自动管理线程的生命周期(创建线程,调度任务,销毁线程等);程序员只需要告诉 GCD 想要如何执行什么任务,不需要编写任何线程管理代码。
2、GCD的核心概念
(1)任务
任务就是要在线程中执行的操作。我们需要将要执行的代码用block封装好,然后将任务添加到队列并指定任务的执行方式,等待CPU从队列中取出任务放到对应的线程中执行。
- queue:队列
- block:任务
// 1.用同步的方式执行任务
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
// 2.用异步的方式执行任务
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
(2)队列
Dispatch Queue,当我们通过dispatch_async等函数把Block加入Dispatch Queue后,Dispatch Queue按照追加的顺序(FIFO)执行处理。

队列的种类:
-Serial Dispatch Queue(串行队列) ——等待现在执行中处理结束再加入队列
-Concurrent Dispatch Queue(并发队列) ——不等待现在执行中处理结束,直接加入队列


下面用代码说明一下:
//创建一个串行队列
dispatch_queue_t serial_queue = dispatch_queue_create("TestQueue", DISPATCH_QUEUE_SERIAL);
//异步执行任务
dispatch_async(serial_queue, ^{
NSLog(@"任务1");
});
dispatch_async(serial_queue, ^{
NSLog(@"任务2");
});
dispatch_async(serial_queue, ^{
NSLog(@"任务3");
});
dispatch_async(serial_queue, ^{
NSLog(@"任务4");
});
打印结果:
2019-11-14 23:38:29.031583+0800 TestApp[5209:541656] 任务1
2019-11-14 23:38:29.031741+0800 TestApp[5209:541656] 任务2
2019-11-14 23:38:29.031834+0800 TestApp[5209:541656] 任务3
2019-11-14 23:38:29.031923+0800 TestApp[5209:541656] 任务4
这里Serial Dispatch Queue只会使用一个线程,因为它是串行队列,只会当一个处理执行完了才会将下一个任务交给线程处理。
//创建一个并发队列
dispatch_queue_t concurrent_queue = dispatch_queue_create("come.tanpei", DISPATCH_QUEUE_CONCURRENT);
//异步执行任务
dispatch_async(concurrent_queue, ^{
NSLog(@"block 1");
});
dispatch_async(concurrent_queue, ^{
NSLog(@"block 2");
});
dispatch_async(concurrent_queue, ^{
NSLog(@"block 3");
});
dispatch_async(concurrent_queue, ^{
NSLog(@"block 4");
});
打印结果:
2019-11-14 23:41:43.247675+0800 TestApp[5250:551396] 任务1
2019-11-14 23:41:43.247676+0800 TestApp[5250:551399] 任务3
2019-11-14 23:41:43.247676+0800 TestApp[5250:551398] 任务2
2019-11-14 23:41:43.247689+0800 TestApp[5250:551397] 任务4
block的执行完成是随机的,因为他们虽然是按顺序把任务提交给线程,但是因为不需要等待前一个任务执行,所以几乎是同时交给线程处理的。所以这里会使用多个线程,而具体线程数的多少由XNU内核决定。
3、DisaPatch Queue的获取
明白了串行队列和并发队列后,我们看一下GCD中关于队列是如何创建或者获取的。
(1)创建队列
在使用Dispatch Queue的时候我们可以通过dispatch_queue_create函数创建队列。例如上面实例代码中的方式。
(2)获取队列
也可以获取系统给我们提供的队列
系统给我们提供了两种队列:
- dispatch_get_main_queue(); 主队列在主线程执行,是串行队列
- dispatch_get_global_queue(); 全局队列在子线程执行,是并发队列

全局并发队列的优先级:
//全局并发队列的优先级
#define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高优先级
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认(中)优先级
//注意,自定义队列的优先级都是默认优先级
#define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低优先级
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台优先级
4、同步和异步
明白了任务,队列(串行队列,并发队列),队列的创建和获取后,接下来我们需要了解另一组核心概念,就是执行任务的方式:同步和异步。
- "同步": 只能在'当前'线程中执行任务,不具备开启新线程的能力.
- "异步": 可以在'新'的线程中执行任务,具备开启新线程的能力.
回看上面的串行队列代码和并发队列代码,我们会发现我们使用异步的方式将任务(Block)添加到队列当中。使用下面的方法:
//异步方式
dispatch_async(queue, ^{
任务
});
//同步方式
dispatch_sync(queue, ^{
任务
});
(1)dispatch_async表示异步:将指定的Block”非同步“加入Dispatch Queue,不做任何等待

(2)dispatch_sync表示同步:将指定的Block”同步“的加入Dispatch Queue,在Block结束之前,dispatch_sync函数会一直等待

所以会出现6中情况:
- 串行队列同步执行,:当前线程-顺序执行
- 串行队列异步执行,开辟一条新的线程,在该线程中顺序执行
- 并行队列同步执行,不开辟线程,在当前线程中顺序执行
- 并行队列异步执行,开辟多个新的线程,并且线程会重用,无序执行
- 主队列异步执行,不开辟新的线程,顺序执行
- 主队列同步执行,会造成死锁('主线程'和'主队列'相互等待,卡住主线程)
5、死锁
由于dispatch_sync会等待Block执行结束才会继续往下执行,所以会产生死锁的情况
我们直接在主线程中同步加入一个Blcok:
dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_sync(main_queue, ^{
NSLog(@"main queue");
});
NSLog(@"go on");
无任何输出,程序直接卡死了。这就是造成了死锁。
因为该源代码在main_queue(主线程)中加入一个加入一个指定的Block,并等待其执行结束。而由于main_queue是一个串行队列,它要等当前线程中的任务处理完后才会把队列中的任务提交到主线程,而主线程又在等待这段代码执行,所以造成了相互等待,就产生了死锁。(而并发队列不会产生死锁)
dispatch_queue_t global_queue = dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_DEFAULT);
dispatch_sync(global_queue, ^{
NSLog(@"global_queue out");
dispatch_sync(global_queue, ^{
NSLog(@"global_queue in");
});
});
所以产生死锁的话一般都是在串行队列中并且是在一个线程中同步往这个线程提交一个Block。
6、Dispatch Group(派发分组)
了解上面的内容,接下来我们了解一下GCD另一个功能,通过派发分组,可以将任务分组执行,调用者在任务执行完毕后会受到通知,并做相应处理。
//创建调度组
dispatch_group_t group = dispatch_group_create();
//将调度组添加到队列,执行 block 任务
dispatch_group_async(group, queue, block);
//当调度组中的所有任务执行结束后,获得通知,统一做后续操作
dispatch_group_notify(group, dispatch_get_main_queue(), block);
下面通过代码来看一下:
dispatch_queue_t global_queue = dispatch_get_global_queue(0, DISPATCH_QUEUE_PRIORITY_DEFAULT);
dispatch_group_t group = dispatch_group_create();
;
dispatch_group_async(group, global_queue, ^{
NSLog(@"task 1");
});
dispatch_group_async(group, global_queue, ^{
NSLog(@"task 2");
});
dispatch_group_async(group, global_queue, ^{
NSLog(@"task 3");
});
dispatch_group_notify(group, global_queue, ^{
NSLog(@"notify");
});
NSLog(@"other task");
打印结果:
2017-09-27 17:06:37.795564+0800 aegewgr[4983:1713915] other task
2017-09-27 17:06:37.795571+0800 aegewgr[4983:1714182] task 3
2017-09-27 17:06:37.795578+0800 aegewgr[4983:1714181] task 1
2017-09-27 17:06:37.795578+0800 aegewgr[4983:1714183] task 2
2017-09-27 17:06:37.795813+0800 aegewgr[4983:1714183] notify
从代码可以看出,dispatch_group_notify并没有阻塞当前线程,而且它提交的Block一定是当group中的所有任务执行完后才会执行。另外,这里的queue可以不是一个queue,你可以使用任意其它queue,不过最好是并发队列,如果是串行队列,任务会按顺序一个一个执行,那使用group的意义就不大了。
如果业务需求,我们要阻塞线程,等待三个任务都执行完毕后才继续往下执行的话,可以使用
dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 1));
该函数设置了一个等待时间也就是说程序要一直阻塞当前线程直到group中的任务执行完毕或者超过等待时间,才会继续往下执行。
7、GCD中其他功能
(1)延时执行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 2秒后异步执行这里的代码...
});
(2)一次性执行
应用场景:保证某段代码在程序运行过程中只被执行一次,在单例设计模式中被广泛使用。
// 使用dispatch_once函数能保证某段代码在程序运行过程中只被执行1次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行1次的代码(这里面默认是线程安全的)
});
(3)Dispatch Semaphore(信号量)
信号量其实就是用来保证访问资源的线程数,当信号量大于等于1时,资源可以访问,否则无法访问资源,直到其它线程释放资源。
这里主要有三个函数:
dispatch_semaphore_t dispatch_semaphore_create(long value); //创建一个dispatch_semaphore_t,value为初始信号量
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); //信号量-1
long dispatch_semaphore_signal(dispatch_semaphore_t dsema); //信号量+1
举个例子:
//crate的value表示,最多几个资源可访问
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//任务1
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run task 1");
sleep(1);
NSLog(@"complete task 1");
dispatch_semaphore_signal(semaphore);
});
//任务2
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run task 2");
sleep(1);
NSLog(@"complete task 2");
dispatch_semaphore_signal(semaphore);
});
//任务3
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run task 3");
sleep(1);
NSLog(@"complete task 3");
dispatch_semaphore_signal(semaphore);
});
输出结果:
2017-09-27 18:04:27.590428+0800 aegewgr[5149:1860224] run task 1
2017-09-27 18:04:27.590428+0800 aegewgr[5149:1860221] run task 2
2017-09-27 18:04:28.591086+0800 aegewgr[5149:1860224] complete task 1
2017-09-27 18:04:28.591086+0800 aegewgr[5149:1860221] complete task 2
2017-09-27 18:04:28.591386+0800 aegewgr[5149:1860219] run task 3
2017-09-27 18:04:29.591845+0800 aegewgr[5149:1860219] complete task 3
信号量也经常和For循环配合使用,设置信号量访问数为1,循环将任务用异步方式执行在子线程中,形成一个在子线程中异步串行执行的效果。
8、GCD可能会遇到的一些问题
GCD的queue、main queue中执行的代码一定是在main thread么?
对于queue中所执行的代码不一定在main thread中。如果queue是在主线程中创建的,那么所执行的代码就是在主线程中执行。如果是在子线程中创建的,那么就不会在main thread中执行。
• 对于main queue就是在主线程中的,因此一定会在主线程中执行。获取main queue就可以了,不需要我们创建,获取方式通过调用方法dispatch_get_main_queue来获取。
如何用GCD同步若干个异步调用?(如根据若干个url异步加载多张图片,然后在都下载完成后合成一张整图)
使用Dispatch Group追加block到Global Group Queue,这些block如果全部执行完毕,就会执行Main Dispatch Queue中的结束处理的block。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, );
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ /*加载图片1 / });
dispatch_group_async(group, queue, ^{ /加载图片2 / });
dispatch_group_async(group, queue, ^{ /加载图片3 */ });
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 合并图片
});
网友评论