前言
GCD的API有很多,学习了比较重要的API,这里学习GCD的实现
GCD是异步执行任务的技术之一,GCD规定我们只需要定义想执行的任务并且追加到适当的Dispatch Queue里,GCD就能生成必要的线程并计划执行任务。
任务和队列
回顾一下任务和队列
学习GCD之前,先来了解 GCD 中两个核心概念:『任务
』 和 『队列
』。
任务
就是执行操作的意思,在线程中执行的那段代码。在 GCD 中是放在 block 中的。执行任务有两种方式:『同步执行』 和 『异步执行』。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力
同步执行和异步执行
同步执行sync
-
- 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
-
- 只能在当前线程中执行任务,不具备开启新线程的能力。
异步执行async
-
- 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
-
- 可以在新的线程中执行任务,具备开启新线程的能力。
-
- 异步执行(async)虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关。
队列
队列(Dispatch Queue
):这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO
(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务
串行队列和并发队列
-
串行队列
每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务
-
并发队列
可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)
并发队列只有在异步执行dispatch_async方法下有效
任务的创建方法
GCD 提供了同步执行任务的创建方法 dispatch_sync 和异步执行任务创建方法 dispatch_async。
// 串行队列的创建方法
dispatch_queue_t serialQueue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t concurrentQueue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
不同的组合
由于上述的不同队列和任务的创建,加上之前的主队列和全局队列,对于全局队列我们可以理解为普通的队列,所以只需要对主队列进行研究这样我们出现了6种研究方法
- 同步执行 + 并发队列
- 异步执行 + 并发队列
- 同步执行 + 串行队列
- 异步执行 + 串行队列
- 同步执行 + 主队列
- 异步执行 + 主队列
『主线程』 中调用 『主队列』+『同步执行』 会导致死锁问题。 - 这是因为 主队列中追加的同步任务 和 主线程本身的任务 两者之间相互等待,阻塞了 『主队列』,最终造成了主队列所在的线程(主线程)死锁问题
- 而如果我们在 『其他线程』 调用 『主队列』+『同步执行』,则不会阻塞 『主队列』,自然也不会造成死锁问题。最终的结果是:不会开启新线程,串行执行任务。
队列嵌套情况下,不同组合方式区别
除了上边提到的『主线程』中调用『主队列』+『同步执行』会导致死锁问题。实际在使用『串行队列』的时候,也可能出现阻塞『串行队列』所在线程的情况发生,从而造成死锁问题。这种情况多见于同一个串行队列的嵌套使用。
比如下面代码这样:在『异步执行』+『串行队列』的任务中,又嵌套了『当前的串行队列』,然后进行『同步执行』。
- (void)lock {
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]); // 打印当前线程
});
});
}
解释:执行上面的代码会导致 串行队列中追加的任务 和 串行队列中原有的任务 两者之间相互等待,阻塞了**『串行队列』**,最终造成了 串行队列所在的线程(子线程)死锁问题。
主队列造成死锁也是基于这个原因,所以,这也进一步说明了主队列其实并不特殊
对于队列和任务,这里简单的总结。
- 同步异步任务可以看成是线程问题,异步就是系统给你开了多个门,让你的人(串行或者并行)通过
- 串行就是一堆人排成一队,而并行就理解为一堆人分成多个队伍进入门(线程,任务)。
『不同队列』+『不同任务』 组合,以及 『队列中嵌套队列』 使用的区别:
『异步执行 + 并发队列』就是:系统开启了多个线程(主线程+其他子线程),任务可以多个同时运行。
『同步执行 + 并发队列』就是:系统只默认开启了一个主线程,没有开启子线程,虽然任务处于并发队列中,但也只能一个接一个执行了。
1. 同步执行 + 并发队列
- 在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务。
/**
* 同步执行 + 并发队列
* 特点:在当前线程中执行任务,不会开启新线程,执行完一个任务,再执行下一个任务。
*/
- (void)syncConcurrent {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"syncConcurrent---begin");
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_sync(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_sync(queue, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
});
NSLog(@"syncConcurrent---end");
}
从 同步执行 + 并发队列 中可看到:
- 所有任务都是在当前线程(主线程)中执行的,没有开启新的线程(同步执行不具备开启新线程的能力)。
- 所有任务都在打印的 syncConcurrent—begin 和 syncConcurrent—end 之间执行的(同步任务 需要等待队列的任务执行结束)。
- 任务按顺序执行的。按顺序执行的原因:虽然 并发队列 可以开启多个线程,并且同时执行多个任务。但是因为本身不能创建新线程,只有当前线程这一个线程(同步任务 不具备开启新线程的能力),所以也就不存在并发。而且当前线程只有等待当前队列中正在执行的任务执行完毕之后,才能继续接着执行下面的操作(同步任务 需要等待队列的任务执行结束)。所以任务只能一个接一个按顺序执行,不能同时被执行。
2. 异步执行 + 并发队列
- 可以开启多个线程,任务交替(同时)执行。
// 串行队列的创建方法
dispatch_queue_t serialQueue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t concurrentQueue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
// 同步执行任务创建方法
dispatch_sync(serialQueue, ^{
// 这里放同步执行任务代码
});
// 异步执行任务创建方法
dispatch_async(concurrentQueue, ^{
// 这里放异步执行任务代码
});
在 异步执行 + 并发队列 中可以看出:
- 除了当前线程(主线程),系统又开启了 3 个线程,并且任务是交替/同时执行的。(异步执行 具备开启新线程的能力。且 并发队列 可开启多个线程,同时执行多个任务)。
- 所有任务是在打印的 asyncConcurrent—begin 和 asyncConcurrent—end 之后才执行的。说明当前线程没有等待,而是直接开启了新线程然后一步一步走新线程完成下去的,在新线程中执行任务(异步执行 不做等待,可以继续执行任务)。
3. 同步执行 + 串行队列
- 不会开启新线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务。.
/**
* 同步执行 + 串行队列
* 特点:不会开启新线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务。
*/
- (void)syncSerial {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"syncSerial---begin");
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_sync(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_sync(queue, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
});
NSLog(@"syncSerial---end");
}
在 同步执行 + 串行队列 可以看到:
- 所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(同步执行 不具备开启新线程的能力)。
- 所有任务都在打印的 syncConcurrent—begin 和 syncConcurrent—end 之间执行(同步任务 需要等待队列的任务执行结束)。
- 任务是按顺序执行的(串行队列 每次只有一个任务被执行,任务一个接一个按顺序执行)
4. 异步执行 + 串行队列
- 会开启新线程,但是因为任务是串行的,执行完一个任务,再执行下一个任务
/**
* 异步执行 + 串行队列
* 特点:会开启新线程,但是因为任务是串行的,执行完一个任务,再执行下一个任务。
*/
- (void)asyncSerial {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"asyncSerial---begin");
dispatch_queue_t queue = dispatch_queue_create("net.bujige.testQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
});
NSLog(@"asyncSerial---end");
}
在 异步执行 + 串行队列 可以看到:
- 开启了一条新线程(异步执行 具备开启新线程的能力,串行队列 只开启一个线程)。
- 所有任务是在打印的 syncConcurrent—begin 和 syncConcurrent—end 之后才开始执行的(异步执行 不会做任何等待,可以继续执行任务)。
- 任务是按顺序执行的(串行队列 每次只有一个任务被执行,任务一个接一个按顺序执行)。
5. 同步执行 + 主队列
主队列:GCD 默认提供的 串行队列。
默认情况下,平常所写代码是直接放在主队列中的。
所有放在主队列中的任务,都会放到主线程中执行。
可使用 dispatch_get_main_queue() 获得主队列。
- 同步执行 + 主队列 在不同线程中调用结果也是不一样,在主线程中调用会发生死锁问题,而在其他线程中调用则不会。
5.1 在主线程中调用 『同步执行 + 主队列』
/**
* 同步执行 + 主队列
* 特点(主线程调用):互等卡主不执行。
* 特点(其他线程调用):不会开启新线程,执行完一个任务,再执行下一个任务。
*/
- (void)syncMain {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"syncMain---begin");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_sync(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_sync(queue, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
});
NSLog(@"syncMain---end");
}
- 打印结果也是截止到了begin,这是因为我们在主线程中执行 syncMain 方法,相当于把 syncMain 任务放到了主线程的队列中。而 同步执行 会等待当前队列中的任务执行完毕,才会接着执行。那么当我们把 任务 1 追加到主队列中,任务 1 就在等待主线程处理完 syncMain 任务。而syncMain 任务需要等待 任务 1 执行完毕,才能接着执行。
- 那么,现在的情况就是 syncMain 任务和 任务 1 都在等对方执行完毕。这样大家互相等待,所以就卡住了,所以我们的任务执行不了,而且 syncMain也不会end
5.2 在其他线程中调用『同步执行 + 主队列』
要是如果不在主线程中调用,而在其他线程中调用会如何呢?
设计到了创建新线程
- 使用 NSThread 的 detachNewThreadSelector 方法会创建线程,并自动启动线程执行 selector 任务
- (void)getOtherThread {
// 使用 NSThread 的 detachNewThreadSelector 方法会创建线程,并自动启动线程执行 selector 任务
[NSThread detachNewThreadSelector:@selector(syncMain) toTarget:self withObject:nil];
}
在其他线程中使用 同步执行 + 主队列 可看到:
- 所有任务都是在主线程(非当前线程)中执行的,没有开启新的线程(所有放在主队列中的任务,都会放到主线程中执行)。
- 所有任务都在打印的 syncConcurrent—begin 和 syncConcurrent—end 之间执行(同步任务 需要等待队列的任务执行结束)。
- 任务是按顺序执行的(主队列是 串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)。
不同于主线程的原因:因为syncMain 任务 放到了其他线程里,而 任务 1、任务 2、任务3 都在追加到主队列中,这三个任务都会在主线程中执行。syncMain 任务 在其他线程中执行到追加 任务 1 到主队列中,因为主队列现在没有正在执行的任务,所以,会直接执行主队列的 任务1,等 任务1 执行完毕,再接着执行 任务 2、任务 3。所以这里不会卡住线程,也就不会造成死锁问题
6. 异步执行 + 主队列
- 只在主线程中执行任务,执行完一个任务,再执行下一个任务。
/**
* 异步执行 + 主队列
* 特点:只在主线程中执行任务,执行完一个任务,再执行下一个任务
*/
- (void)asyncMain {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"asyncMain---begin");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
});
NSLog(@"asyncMain---end");
}
在 异步执行 + 主队列 可以看到:
- 所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(虽然 异步执行 具备开启线程的能力,但因为是主队列,所以所有任务都在主线程中)。
- 所有任务是在打印的 syncConcurrent—begin 和 syncConcurrent—end 之后才开始执行的** ( 异步执行不会做任何等待,可以继续执行任务 **
- 任务是按顺序执行的(因为主队列是 串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)。
GCD 线程间的通信
根据GCD的需求,我们一般把比较耗费时间的操作,放在其他的线程里面,而对于UI界面的刷新,点击等响应事件则是主线程进行的操作
而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。
/**
* 线程间通信
*/
- (void)communication {
// 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 获取主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(queue, ^{
// 异步追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
// 回到主线程
dispatch_async(mainQueue, ^{
// 追加在主线程中执行的任务
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
});
}
总结
GCD的实现和API一样重要,主要解释了如何将串行并行和同步异步任务结合达到提升进程的效果。