本文为学习笔记,主要是参考文章内容的简化版,加深理解, 复习使用。
参考文章
一、使用GCD的好处
-
可用于多核的并行运算
-
会自动利用更多的CPU内核(比如双核、四核)
-
会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
-
程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码
二、GCD任务和队列
GCD的两个核心概念:【任务】和【队列】
1. 任务
任务,就是执行操作的意思,换句话说就是你在线程中执行的那段代码;
执行任务有两种方式:【同步执行】和【异步执行】。两者主要的区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。
-
同步执行(sync):
-
同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里的任务完成后再继续执行;
-
只能在当前线程中执行任务,不具备开启新线程的能力。
-
-
异步执行(async):
-
异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务;
-
可以在新的线程中执行任务,具备开启新线程的能力(并不一定开启新线程)。
-
同步执行代码举例:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"准备开启线程, 当前线程:%@", [NSThread currentThread]);
// 这里如果使用 "dispatch_get_main_queue()"主队列, 会造成死锁
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"开始执行任务, 当前线程:%@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:2];
NSLog(@"结束任务, 当前线程:%@", [NSThread currentThread]);
});
NSLog(@"执行后续任务(代码), 当前线程:%@", [NSThread currentThread]);
}
输出结果:
准备开启线程, 当前线程:<_NSMainThread: 0x60000022c840>{number = 1, name = main}
开始执行任务, 当前线程:<_NSMainThread: 0x60000022c840>{number = 1, name = main}
结束任务, 当前线程:<_NSMainThread: 0x60000022c840>{number = 1, name = main}
执行后续任务(代码), 当前线程:<_NSMainThread: 0x60000022c840>{number = 1, name = main}
从上述代码和输出日志可以看出,将同步执行任务添加进队列后,该同步任务执行完成后,才会继续执行后续代码。
死锁说明:程序将viewDidLoad方法添加进主队列,所以在viewDidLoad方法执行完成后,主线程才会执行后续任务。但是在viewDidLoad方法中,又将一个同步执行任务添加进主队列,该同步任务必须等待主队列中的上一个任务执行结束,也就是viewDidLoad方法执行结束后,才能执行。这就造成了死锁。
异步执行代码举例:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"准备开启线程, 当前线程:%@", [NSThread currentThread]);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"开始执行任务, 当前线程:%@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:2];
NSLog(@"结束任务, 当前线程:%@", [NSThread currentThread]);
});
NSLog(@"执行后续任务(代码), 当前线程:%@", [NSThread currentThread]);
}
输出结果:
准备开启线程, 当前线程:<_NSMainThread: 0x600002f789c0>{number = 1, name = main}
执行后续任务(代码), 当前线程:<_NSMainThread: 0x600002f789c0>{number = 1, name = main}
开始执行任务, 当前线程:<NSThread: 0x600002f1c980>{number = 6, name = (null)}
结束任务, 当前线程:<NSThread: 0x600002f1c980>{number = 6, name = (null)}
从上述代码和输出日志可以看出,将异步执行任务添加进队列后,会继续执行后续代码,而该异步执行任务在2s后才执行完。
2. 队列
队列(Dispatch Queue),这里的队列指执行任务的等待队列,即用来等待任务的队列。队列是一种特殊的线性表,遵循FIFO(先进先出)的原则。
在GCD中有两种队列:【串行队列】和【并发队列】。主要区别:执行顺序不同,以及开启线程数不同。
-
串行队列(Serial Dispatch Queue)
每次只有一个任务被执行。让任务一个接一个地执行。(只开启一个线程,一个任务执行完毕后再执行下一个任务)
-
并发队列(Concurrent Dispatch Queue)
可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务),并发功能只在异步(dispatch_async)方法下才有效
在这里明确一下 任务、队列、线程这三者的关系。最开始以为dispatch_get_main_queue() 方法拿到的就是主线程,dispatch_get_global_queue() 方法拿到的就是子线程。但其实这两个方法拿到的是队列。
dispatch_async(dispatch_get_main_queue, ^{})方法做的是:将任务(block内的代码)加入到队列,然后系统从队列取出任务(先进先出),再使用相应的线程去执行任务。
三、GCD的使用步骤
-
创建一个队列
-
将任务追加到等待队列中,然后系统会根据任务类型去执行任务(同步执行或异步执行)
3.1 队列的创建/获取
// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT)
-
创建方法需要传入两个参数:
-
第一个参数表示队列的唯一标识符,用于DEBUG,可为空。推荐使用应用程序ID这种逆序全程域名。
-
第二个参数用来标识是串行队列还是并发队列。DISPATCH_QUEUE_SERIAL 表示串行队列,DISPATCH_QUEUE_CONCURRENT 表示并发队列。
-
-
串行队列,GCD默认提供了主队列(Main Dispatch Queue)
-
所以放在主队列的任务,都会放到主线程执行
-
可使用 dispatch_get_main_queue() 方法获得主队列
-
注:主队列其实并不特殊。主队列实质上就是一个普通的串行队列,只是因为默认情况下,当前代码是放在主队列中的,然后主队列中的代码又都会放到主线程中执行,所以造成了主队列的特殊现象。
3.2 任务和队列不同组合方式的区别
默认当前线程为主线程的情况下(暂不考虑队列中嵌套队列的复杂情况):
| 区别 | 并发队列 | 串行队列 | 主队列 |
|---|---|---|---|
| 同步(sync) | 没有开启新线程,串行执行任务 | 没有开启新线程,串行执行任务 | 死锁,卡住不执行 |
| 异步(async) | 有开启新线程,并发执行任务 | 有开启新线程(1条),串行执行任务 | 没有开启新线程,串行执行任务 |
关于死锁,实际上在使用 串行队列 的时候,也可能会出现死锁问题。多见于同一个串行队列的嵌套使用。
如在 异步执行 + 串行队列 的任务中,又嵌套了 同步执行 + 当前队列,代码如下
dispatch_queue_t queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 异步执行 + 串行队列
dispatch_sync(queue, ^{ // 同步执行 + 当前串行队列
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
});
3.3 对任务和队列开启线程能力的理解
假设现在有 5 个人要穿过一道门禁,这道门禁总共有 10 个入口,管理员可以决定同一时间打开几个入口,可以决定同一时间让一个人单独通过还是多个人一起通过。不过默认情况下,管理员只开启一个入口,且一个通道一次只能通过一个人。
这个故事里,人好比是 任务,管理员好比是 系统,入口则代表 线程。
5 个人表示有 5 个任务,10 个入口代表 10 条线程。
- 串行队列 好比是 5 个人排成一支长队。
- 并发队列 好比是 5 个人排成多支队伍,比如 2 队,或者 3 队。
- 同步任务 好比是管理员只开启了一个入口(当前线程)。
- 异步任务 好比是管理员同时开启了多个入口(当前线程 + 新开的线程)。
『异步执行 + 并发队列』 可以理解为:现在管理员开启了多个入口(比如 3 个入口),5 个人排成了多支队伍(比如 3 支队伍),这样这 5 个人就可以 3 个人同时一起穿过门禁了。
『同步执行 + 并发队列』 可以理解为:现在管理员只开启了 1 个入口,5 个人排成了多支队伍。虽然这 5 个人排成了多支队伍,但是只开了 1 个入口啊,这 5 个人虽然都想快点过去,但是 1 个入口一次只能过 1 个人,所以大家就只好一个接一个走过去了,表现的结果就是:顺次通过入口。
换成 GCD 里的语言就是说:
- 『异步执行 + 并发队列』就是:系统开启了多个线程(主线程+其他子线程),任务可以多个同时运行。
- 『同步执行 + 并发队列』就是:系统只默认开启了一个主线程,没有开启子线程,虽然任务处于并发队列中,但也只能一个接一个执行了。
四、GCD的使用
以上的内容主要是概念方面的理解,具体代码操作详见参考文章iOS多线程:『GCD』详尽总结,模块4 至 模块6