本文是GCD多线程编程基础内容的小结,通过本文,你可以了解到:
- 多线程的几个基本概念:进程与线程、串行与并发
- GCD中的2个核心内容:队列、任务
- GCD的基本使用步骤
- GCD中使用同步异步方式添加任务到串行并发队列后执行的实际效果
- GCD中产生死锁的原因以及实际开发中如何避免死锁crash
GCD
Apple为了让开发者更加容易的使用设备上的多核CPU,苹果在 OS X 10.6 和 iOS 4 中引入了 Grand Central Dispatch(GCD),它是 Apple 开发的一个多核编程的较新的解决方法,它主要用于优化应用程序以支持多核处理器,它是一个在线程池模式的基础上执行的并发任务,是我们平常开发中最常见的一种多线程编程方式
多线程基本概念
进程与线程
对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。
有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。
由于每个进程至少要干一件事,所以,一个进程至少有一个线程。当然,像Word这种复杂的进程可以有多个线程,多个线程可以同时执行,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。当然,真正地同时执行多线程需要多核CPU才可能实现。
串行和并发
并发就是多个任务在执行的过程中,时间互相重叠,一个任务执行没结束,另一个已经开始。
串行就是任务一个一个的执行,时间上不相互重叠,一个任务执行结束,下一个任务才能开始执行。
GCG队列
队列是一种特殊的线性表,特殊之处在于它只允许在表的后端进入插入操作,在表的前端进行删除操作,即遵循FIFO
原则。
GCD中的队列(Dispatch Queue
)就是指用来执行任务的等待队列,当我们添加任务到队列之后,开发者不用再直接跟线程
打交道了,只需要向队列中添加代码块即可,GCD 在后端管理着一个线程池。GCD 不仅决定着你的代码块将在哪个线程被执行,它还根据可用的系统资源对这些线程进行管理。这样可以将开发者从线程管理的工作中解放出来,通过集中的管理线程,来缓解大量线程被创建的问题。
GCD中的队列可以分为以下2种:
-
串行队列 ( Serial Dispatch Queue )
串行队列(也称为私有调度队列)按照将他们添加到队列顺序一次执行一个任务。当前正在执行的任务在由队列管理的不同线程(可能因任务而异)上运行。串行队列通常用于同步对特定资源的访问。
-
并发队列 ( Concurrent Dispatch Queue )
并发队列(也称为一种全局调度队列)同时执行一个或多个任务,但任务仍按其添加到队列的顺序启动。当前正在执行的任务在由调度队列管理的不同线程上运行。在任何给定点执行的任务的确切数量是可变的,取决于系统条件。
在我们平时的开发中,还有2种我们最常见的,也是使用频率最高的队列:
-
主队列 ( Main Dispatch Queue )
主队列是一个全局可用的串行队列,它在应用程序的主线程上执行任务。此队列与应用程序的
Runloop
一起工作,将有序任务的执行与附加到Runloop
的其他事件源的执行交错。因为它在应用程序的主线程上运行,所以主队列通常用作应用程序的关键同步点。主队列下的任务不管是异步任务还是同步任务都不会开辟线程,任务只会在主线程顺序执行
-
全局并发队列 ( Global Dispatch Queue )
全局并发队列本质上是一个并发队列,有系统提供,方便编程,可以不用创建就可以直接使用
GCD任务
任务就是你要在线程中执行的代码,在GCD中是用Block来定义任务的,是用起来非常灵活便捷。
GCD中执行任务的方式有两种:同步执行(sync)与异步执行(async)
-
同步执行
同步执行就是指使用
dispatch_sync
方法将任务同步的添加到队列里,在添加的任务执行结束之前,当前线程会被阻塞,然后会一直等待,直到任务完成。dispatch_sync
添加的任务只能在当前线程执行,不具备开启新线程的能力 -
异步执行
异步执行就是指使用
dispatch_async
方法将任务异步的添加到队列里,它不需要等待任务执行结束,不需要做任何等待就能继续执行任务dispatch_async
添加的任务可以在新的线程中执行任务,具备开启新线程的能力,但并不一定会开启新线程
GCD的使用步骤
这个就跟赵本山跟宋丹丹的小品《钟点工》里提出的把大象装进冰箱的经典问题一样,都是分三步:
把大象装进冰箱
- 把冰箱门打开
- 把大象装进去
- 把冰箱门关上
GCD使用步骤
- 创建或获取一个队列
- 定制需要执行的任务
- 将任务追加到队列
创建或获取一个队列
使用
dispatch_get_main_queue()
获取主队列。-
使用
dispatch_get_global_queue
获取全局并发队列,这个函数有2个参数,第一个参数是全局队列的优先级,一般情况下,使用的都是DISPATCH_QUEUE_PRIORITY_DEFAULT
优先级,第二个参数是一个保留字段,我们需要给它一个0
,否则这个函数会返回一个NULL
,导致我们获取不到正常的全局队列。Reserved for future use. Passing any value other than zero may result in a NULL return value.
-
使用
dispatch_queue_create
函数,创建自定义的串行或并行队列,这个函数的定义如下:* @param label * A string label to attach to the queue. * This parameter is optional and may be NULL. * * @param attr * A predefined attribute such as DISPATCH_QUEUE_SERIAL, * DISPATCH_QUEUE_CONCURRENT, or the result of a call to * a dispatch_queue_attr_make_with_* function. * * @result * The newly created dispatch queue. */ API_AVAILABLE(macos(10.6), ios(4.0)) DISPATCH_EXPORT DISPATCH_MALLOC DISPATCH_RETURNS_RETAINED DISPATCH_WARN_RESULT DISPATCH_NOTHROW dispatch_queue_t dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr);
dispatch_queue_create
函数最后返回了一个队列dispatch_queue_t
,这个函数有2个参数,第一个参数其实可以看成是是我们给这个队列取的名字,以便后续debug
,苹果官方是推荐开发者使用逆序全程域名。第二个参数,是用于确定这个队列是串行队列还是并发队列,使用
DISPATCH_QUEUE_CONCURRENT
表示创建的队列是并发队列,使用DISPATCH_QUEUE_SERIAL
或者NULL
表示创建的队列是串行队列,它们两个其实是等价的,见下面的注释:/*! * @const DISPATCH_QUEUE_SERIAL * * @discussion * An attribute that can be used to create a dispatch queue that invokes blocks * serially in FIFO order. * * See dispatch_queue_serial_t. */ #define DISPATCH_QUEUE_SERIAL NULL
不过个人不推荐使用
NULL
的方式来表示创建的是串行队列,这种方式在多人开发时,阅读性是比较差的。以下是获取或创建的4种方式:
//获取自定义串行队列 self.serialQueue = dispatch_queue_create("com.zed.customSerialQueue", DISPATCH_QUEUE_SERIAL); //获取自定义并发队列 self.concurrentQueue = dispatch_queue_create("com.zed.customConcurrentQueue", DISPATCH_QUEUE_CONCURRENT); //获取主队列 dispatch_queue_t mainQueue = dispatch_get_main_queue(); //获取全局并发队列 dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
定制需要执行的任务
GCD种的任务其实就是一个Block
,就是我们俗称的代码块,在这个代码块里面,把我们需要做的事情就是,将我们的任务代码加入到这个block中
void (^block)(void) = ^{
NSLog(@"执行任务");
for (int i = 0; i<100; i++) {
NSLog(@"%d",i);
}
NSLog(@"Thread:%@",[NSThread currentThread]);
};
将任务追加到队列
GCD提供了2个方法用于将任务追加到队列:
-
dispatch_sync
使用同步执行的方式追加到队列 -
dispatch_async
使用异步的方式追加到队列
//三、将任务增加到队列中
dispatch_async(globalQueue, block);
GCD的基本使用
前面我们已经介绍了两种基本队列(串行队列与并发队列),两种特殊队列(主队列与全局并发队列),两种任务执行方式(同步执行与异步执行),所以,我们就有了8中不同的组合方式,不过由于全局并发队列跟普通并发队列的性质是差不多的,所以,我们就有6中不同的组合,接下来,我们从3个角度来观察这6种组合方式的效果:
三个角度
- 是否开启线程
- 任务是按序执行还是交替(同时)执行
- 是否阻塞当前线程
六种组合方式
- 同步执行+并发队列
- 异步执行+并发队列
- 同步执行+串行队列
- 异步执行+串行队列
- 同步执行+主队列
- 异步执行+主队列
同步执行+并发队列
#pragma mark - 同步执行+并发队列
/*
* 特点:
* 1.在当前线程中执行任务,不会开启新线程
* 2.按序执行任务,执行行完一个任务,再执行下一个任务
* 3.会阻塞当前线程
*/
- (IBAction)executeSyncConcurrencyTask:(UIButton *)sender {
NSLog(@"CurrentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"SyncConcurrencyTask---begin");
dispatch_sync(self.concurrentQueue, ^{
// 追加任务1
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
}
});
dispatch_sync(self.concurrentQueue, ^{
// 追加任务2
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
}
});
dispatch_sync(self.concurrentQueue, ^{
// 追加任务3
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
}
});
NSLog(@"SyncConcurrencyTask---end");
NSLog(@"*********************************************************");
}
执行结果如下:
2019-04-22 13:42:07.265811+0800 GCD(一) 队列、任务、串行、并发[8668:1868026] CurrentThread---<NSThread: 0x600002969400>{number = 1, name = main}
2019-04-22 13:42:07.265945+0800 GCD(一) 队列、任务、串行、并发[8668:1868026] SyncConcurrencyTask---begin
2019-04-22 13:42:09.266681+0800 GCD(一) 队列、任务、串行、并发[8668:1868026] 1---<NSThread: 0x600002969400>{number = 1, name = main}
2019-04-22 13:42:11.268049+0800 GCD(一) 队列、任务、串行、并发[8668:1868026] 1---<NSThread: 0x600002969400>{number = 1, name = main}
2019-04-22 13:42:12.299360+0800 GCD(一) 队列、任务、串行、并发[8668:1868133] XPC connection interrupted
2019-04-22 13:42:13.269544+0800 GCD(一) 队列、任务、串行、并发[8668:1868026] 2---<NSThread: 0x600002969400>{number = 1, name = main}
2019-04-22 13:42:15.270540+0800 GCD(一) 队列、任务、串行、并发[8668:1868026] 2---<NSThread: 0x600002969400>{number = 1, name = main}
2019-04-22 13:42:17.271936+0800 GCD(一) 队列、任务、串行、并发[8668:1868026] 3---<NSThread: 0x600002969400>{number = 1, name = main}
2019-04-22 13:42:19.273478+0800 GCD(一) 队列、任务、串行、并发[8668:1868026] 3---<NSThread: 0x600002969400>{number = 1, name = main}
2019-04-22 13:42:19.273713+0800 GCD(一) 队列、任务、串行、并发[8668:1868026] SyncConcurrencyTask---end
2019-04-22 13:42:19.273857+0800 GCD(一) 队列、任务、串行、并发[8668:1868026] *********************************************************
通过我们的代码测验,可以看出:
所有的任务都是在主线程(当前线程)中执行的,并没有开启新的线程,这也说明了同步执行的一个特性:同步执行任务不具备开启新线程的能力。
任务1、任务2、任务3是按顺序执行的,并没有出现并发执行的情况,这是因为虽然并发队列具备同时执行多个任务的能力,但是由于是同步执行不具备开启新线程的能力,所以,即使任务被追加到了并发队列,它也没有办法去开启新的线程,只能在当前线程中执行任务。
从我们的log中可以看出,我们所有的任务都是在
begin
与end
之间的,所以说,它会阻塞当前线程,等待队列中的任务执行结束,才会继续执行下面的代码。
异步执行+并发队列
#pragma mark - 异步执行+并发队列
/*
* 特点:
* 1.开启多个新线程执行任务
* 2.任务交替(同时)执行
* 3.不会阻塞当前线程
*/
- (IBAction)executeAsyncConcurrencyTask:(UIButton *)sender {
NSLog(@"CurrentThread begin---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"AsyncConcurrencyTask---begin");
dispatch_async(self.concurrentQueue, ^{
// 追加任务1
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
}
});
dispatch_async(self.concurrentQueue, ^{
// 追加任务2
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
}
});
dispatch_async(self.concurrentQueue, ^{
// 追加任务3
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
}
});
NSLog(@"CurrentThread end---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"AsyncConcurrencyTask---end");
NSLog(@"*********************************************************");
}
执行结果如下:
2019-04-22 14:32:14.941222+0800 GCD(一) 队列、任务、串行、并发[9417:2009244] CurrentThread begin---<NSThread: 0x6000000cea40>{number = 1, name = main}
2019-04-22 14:32:14.941405+0800 GCD(一) 队列、任务、串行、并发[9417:2009244] AsyncConcurrencyTask---begin
2019-04-22 14:32:14.941608+0800 GCD(一) 队列、任务、串行、并发[9417:2009244] CurrentThread end---<NSThread: 0x6000000cea40>{number = 1, name = main}
2019-04-22 14:32:14.941757+0800 GCD(一) 队列、任务、串行、并发[9417:2009244] AsyncConcurrencyTask---end
2019-04-22 14:32:14.941894+0800 GCD(一) 队列、任务、串行、并发[9417:2009244] *********************************************************
2019-04-22 14:32:16.945860+0800 GCD(一) 队列、任务、串行、并发[9417:2009294] 1---<NSThread: 0x6000000a2380>{number = 4, name = (null)}
2019-04-22 14:32:16.945909+0800 GCD(一) 队列、任务、串行、并发[9417:2011461] 3---<NSThread: 0x6000000a2340>{number = 6, name = (null)}
2019-04-22 14:32:16.945909+0800 GCD(一) 队列、任务、串行、并发[9417:2011460] 2---<NSThread: 0x6000000aec00>{number = 5, name = (null)}
2019-04-22 14:32:18.951120+0800 GCD(一) 队列、任务、串行、并发[9417:2011460] 2---<NSThread: 0x6000000aec00>{number = 5, name = (null)}
2019-04-22 14:32:18.951121+0800 GCD(一) 队列、任务、串行、并发[9417:2011461] 3---<NSThread: 0x6000000a2340>{number = 6, name = (null)}
2019-04-22 14:32:18.951120+0800 GCD(一) 队列、任务、串行、并发[9417:2009294] 1---<NSThread: 0x6000000a2380>{number = 4, name = (null)}
通过我们的代码测验,可以看出:
- 除了在主线程执行的2个log任务之外,系统又开启了3个线程用于执行追加的三个任务,说明异步执行具备开启新线程的能力,并且并发队列可以开启多个线程,交替执行多个任务。
- 从我们的log中可以看到,
begin
的log之后,马上就是end
的log,因此可以看出,它并不会阻塞当前线程,并不需要等待追加的任务执行完成。
同步执行+串行队列
#pragma mark - 同步执行+串行队列
/*
* 特点:
* 1.在当前线程中执行任务,不会开启新线程
* 2.按序执行任务,执行行完一个任务,再执行下一个任务
* 3.会阻塞当前线程
*/
- (IBAction)executeSyncSerialTask:(UIButton *)sender {
NSLog(@"CurrentThread begin---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"SyncSerialTask---begin");
dispatch_sync(self.serialQueue, ^{
// 追加任务1
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
}
});
dispatch_sync(self.serialQueue, ^{
// 追加任务2
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
}
});
dispatch_sync(self.serialQueue, ^{
// 追加任务3
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
}
});
NSLog(@"CurrentThread end---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"SyncSerialTask---end");
NSLog(@"*********************************************************");
}
执行结果如下:
2019-04-22 15:02:52.760352+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] CurrentThread begin---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:02:52.760558+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] SyncSerialTask---begin
2019-04-22 15:02:54.761971+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] 1---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:02:56.762653+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] 1---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:02:58.764202+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] 2---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:03:00.765234+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] 2---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:03:02.766464+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] 3---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:03:04.767966+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] 3---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:03:04.768230+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] CurrentThread end---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:03:04.768379+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] SyncSerialTask---end
2019-04-22 15:03:04.768516+0800 GCD(一) 队列、任务、串行、并发[9826:2087150] *********************************************************
通过我们的代码测验,可以看出:
- 所有的任务都是在主线程(当前线程)中执行的,并且是顺序执行的,没有开启新的线程。
- 从我们的log中可以看出,我们所有的任务都是在
begin
与end
之间的,所以说,它会阻塞当前线程,等待队列中的任务执行结束,才会继续执行下面的代码。
异步执行+串行队列
#pragma mark - 异步执行+串行队列
/*
* 特点:
* 1.会开启一条新线程
* 2.按序执行任务,执行行完一个任务,再执行下一个任务
* 3.不会阻塞当前线程
*/
- (IBAction)executeAsyncSerialTask:(UIButton *)sender {
NSLog(@"CurrentThread begin---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"AsyncSerialTask---begin");
dispatch_async(self.serialQueue, ^{
// 追加任务1
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
}
});
dispatch_async(self.serialQueue, ^{
// 追加任务2
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
}
});
dispatch_async(self.serialQueue, ^{
// 追加任务3
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
}
});
NSLog(@"CurrentThread end---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"AsyncSerialTask---end");
NSLog(@"*********************************************************");
}
执行结果如下:
2019-04-22 15:25:00.103488+0800 GCD(一) 队列、任务、串行、并发[10181:2154024] CurrentThread begin---<NSThread: 0x6000019c9400>{number = 1, name = main}
2019-04-22 15:25:00.103734+0800 GCD(一) 队列、任务、串行、并发[10181:2154024] AsyncSerialTask---begin
2019-04-22 15:25:00.103888+0800 GCD(一) 队列、任务、串行、并发[10181:2154024] CurrentThread end---<NSThread: 0x6000019c9400>{number = 1, name = main}
2019-04-22 15:25:00.103986+0800 GCD(一) 队列、任务、串行、并发[10181:2154024] AsyncSerialTask---end
2019-04-22 15:25:00.104091+0800 GCD(一) 队列、任务、串行、并发[10181:2154024] *********************************************************
2019-04-22 15:25:02.108899+0800 GCD(一) 队列、任务、串行、并发[10181:2154074] 1---<NSThread: 0x600001992fc0>{number = 4, name = (null)}
2019-04-22 15:25:04.111910+0800 GCD(一) 队列、任务、串行、并发[10181:2154074] 1---<NSThread: 0x600001992fc0>{number = 4, name = (null)}
2019-04-22 15:25:06.116733+0800 GCD(一) 队列、任务、串行、并发[10181:2154074] 2---<NSThread: 0x600001992fc0>{number = 4, name = (null)}
2019-04-22 15:25:08.117706+0800 GCD(一) 队列、任务、串行、并发[10181:2154074] 2---<NSThread: 0x600001992fc0>{number = 4, name = (null)}
2019-04-22 15:25:10.122737+0800 GCD(一) 队列、任务、串行、并发[10181:2154074] 3---<NSThread: 0x600001992fc0>{number = 4, name = (null)}
2019-04-22 15:25:12.126742+0800 GCD(一) 队列、任务、串行、并发[10181:2154074] 3---<NSThread: 0x600001992fc0>{number = 4, name = (null)}
通过我们的代码测验,可以看出:
- 三个追加的任务都是在一个新的线程中执行的,在串行队列中异步执行任务,会开启一条新线程,由于队列是串行的,所以任务是按序执行的。
- 从我们的log中可以看到,
begin
的log之后,马上就是end
的log,因此可以看出,它并不会阻塞当前线程,并不需要等待追加的任务执行完成。
异步执行+主队列
#pragma mark - 异步执行+主队列
/*
* 特点:
* 1.在当前线程(主线程)中执行任务
* 2.按序执行任务,执行行完一个任务,再执行下一个任务
* 3.不会阻塞当前线程
*/
- (IBAction)executeAsyncMainQueueTask:(UIButton *)sender {
NSLog(@"CurrentThread begin---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"AsyncMainQueueTask---begin");
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(mainQueue, ^{
// 追加任务1
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
}
});
dispatch_async(mainQueue, ^{
// 追加任务2
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
}
});
dispatch_async(mainQueue, ^{
// 追加任务3
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
}
});
NSLog(@"CurrentThread end---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"AsyncMainQueueTask---end");
NSLog(@"*********************************************************");
}
执行结果如下:
2019-04-22 18:28:03.990381+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] CurrentThread begin---<NSThread: 0x600003ee6900>{number = 1, name = main}
2019-04-22 18:28:03.990589+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] AsyncMainQueueTask---begin
2019-04-22 18:28:03.990808+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] CurrentThread end---<NSThread: 0x600003ee6900>{number = 1, name = main}
2019-04-22 18:28:03.990959+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] AsyncMainQueueTask---end
2019-04-22 18:28:03.991088+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] *********************************************************
2019-04-22 18:28:05.993620+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] 1---<NSThread: 0x600003ee6900>{number = 1, name = main}
2019-04-22 18:28:07.994036+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] 1---<NSThread: 0x600003ee6900>{number = 1, name = main}
2019-04-22 18:28:09.995547+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] 2---<NSThread: 0x600003ee6900>{number = 1, name = main}
2019-04-22 18:28:11.997136+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] 2---<NSThread: 0x600003ee6900>{number = 1, name = main}
2019-04-22 18:28:13.997717+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] 3---<NSThread: 0x600003ee6900>{number = 1, name = main}
2019-04-22 18:28:15.998158+0800 GCD(一) 队列、任务、串行、并发[12894:2615623] 3---<NSThread: 0x600003ee6900>{number = 1, name = main}
通过我们的代码测验,可以看出:
3个任务在主线程中,按序执行
从我们的log中可以看到,
begin
的log之后,马上就是end
的log,因此可以看出,它并不会阻塞当前线程,并不需要等待追加的任务执行完成。
同步执行+主队列
#pragma mark - 同步执行+主队列
/*
* 特点:
* 会直接产生死锁
*/
- (IBAction)executeSyncMainQueueTask:(UIButton *)sender {
NSLog(@"CurrentThread begin---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"SyncMainQueueTask---begin");
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_sync(mainQueue, ^{
// 追加任务1
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
}
});
dispatch_sync(mainQueue, ^{
// 追加任务2
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
}
});
dispatch_sync(mainQueue, ^{
// 追加任务3
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
}
});
NSLog(@"CurrentThread end---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"SyncMainQueueTask---end");
NSLog(@"*********************************************************");
}
执行结果如下:
2019-04-22 16:04:57.238644+0800 GCD(一) 队列、任务、串行、并发[10673:2238846] CurrentThread begin---<NSThread: 0x6000038ca800>{number = 1, name = main}
2019-04-22 16:04:57.238915+0800 GCD(一) 队列、任务、串行、并发[10673:2238846] SyncMainQueueTask---begin
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
frame #0: 0x000000010d45fa19 libdispatch.dylib`__DISPATCH_WAIT_FOR_QUEUE__ + 444
frame #1: 0x00007ffee542deb0
frame #2: 0x000000010d45f3f0 libdispatch.dylib`_dispatch_sync_f_slow + 231
* frame #3: 0x000000010a7cff5a GCD(一) 队列、任务、串行、并发`-[ViewController executeSyncMainQueueTask:](self=0x00007fb8f25124f0, _cmd="executeSyncMainQueueTask:", sender=0x00007fb8f2515fa0) at ViewController.m:220
frame #4: 0x000000010e9b9ecb UIKitCore`-[UIApplication sendAction:to:from:forEvent:] + 83
frame #5: 0x000000010e3f50bd UIKitCore`-[UIControl sendAction:to:forEvent:] + 67
frame #6: 0x000000010e3f53da UIKitCore`-[UIControl _sendActionsForEvents:withEvent:] + 450
frame #7: 0x000000010e3f431e UIKitCore`-[UIControl touchesEnded:withEvent:] + 583
frame #8: 0x000000010e9f50a4 UIKitCore`-[UIWindow _sendTouchesForEvent:] + 2729
frame #9: 0x000000010e9f67a0 UIKitCore`-[UIWindow sendEvent:] + 4080
frame #10: 0x000000010e9d4394 UIKitCore`-[UIApplication sendEvent:] + 352
frame #11: 0x000000010eaa95a9 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 3054
frame #12: 0x000000010eaac1cb UIKitCore`__handleEventQueueInternal + 5948
frame #13: 0x000000010bab8721 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
frame #14: 0x000000010bab7f93 CoreFoundation`__CFRunLoopDoSources0 + 243
frame #15: 0x000000010bab263f CoreFoundation`__CFRunLoopRun + 1263
frame #16: 0x000000010bab1e11 CoreFoundation`CFRunLoopRunSpecific + 625
frame #17: 0x00000001141491dd GraphicsServices`GSEventRunModal + 62
frame #18: 0x000000010e9b881d UIKitCore`UIApplicationMain + 140
frame #19: 0x000000010a7d03c0 GCD(一) 队列、任务、串行、并发`main(argc=1, argv=0x00007ffee542ff90) at main.m:14
frame #20: 0x000000010d4c7575 libdyld.dylib`start + 1
(lldb)
通过我们的代码测验,可以看出:
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0) frame #0: 0x000000010d45fa19 libdispatch.dylib`__DISPATCH_WAIT_FOR_QUEUE__ + 444
应用在主线程同步执行第一个任务时,就会直接crash,我们同步
LLDB
的bt
指定查看函数调用栈,可以发现,在系统库libdispatch
调用__DISPATCH_WAIT_FOR_QUEUE__
函数时,就会产生一个由队列引起的循环等待导致的crash,这就是我们常说的Deadlock
死锁,接下里我们来详细介绍一下死锁产生的原因与注意事项。
GCD中产生死锁的原因
- (IBAction)executeSyncMainQueueTask:(UIButton *)sender {
NSLog(@"CurrentThread begin---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"SyncMainQueueTask---begin");
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_sync(mainQueue, ^{
// 追加任务1
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
}
});
从上面的代码我们可以看出,当我们点击按钮,调用executeSyncMainQueueTask
方法时,这时我们其实是在主队列(串行队列)提交了一个任务,我们暂先称它为任务0
然后我们又使用dispatch_sync
同步执行方法往主队列中提交了任务1
的Block
,现在我们来分析一下,为什么这种情况下会产生死锁
- 先往主队列(串行队列)中提交了任务0,然后在任务0执行的过程中同步地往主队列中添加了任务1
- 主队列中添加的任务都会在主线程中执行,同时按照串行队列的特点(任务按序执行),主线程中首先会执行任务0,任务0执行完成之后才会去执行任务1,但是在任务0执行的过程中,使用同步方式往主队列中添加任务1,由于是使用同步方式,这时主线程会被阻塞,需要任务1完成之后,任务0才会继续往下执行。由此,我们可以看出,由于串行队列的特性,任务1会依赖于任务0的执行完成才会继续往下执行,同时由于同步添加任务的特性(会阻塞当前线程,直到添加的任务执行完成),任务0会依赖于任务1的执行完成。所以,2个任务的执行就会因为相互等待对方的完成,而导致死锁。
通过上面的分析,我们可以看出这里产生死锁的一个很重要的原因就是主队列是一个串行的队列(主队列中只有一条主线程)。如果我们如下例,在并发队列中提交,则不会造成死锁:
dispatch_async(dispatch_get_global_queue(0, 0), ^{ //这里是为了保证当前任务是处于并发队列开辟的线程中,而不是主线程中
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务0");
});
NSLog(@"任务1");
});
原因是并发队列中的任务执行时并行的,所以,任务1并不会一直等待任务0执行完成,才去执行,而是直接执行完。因此任务0因为任务1的结束,线程阻塞也会被消除,任务0得以继续执行。
我们再开看一组示例:
//目前处于主线程中
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务0");
});
NSLog(@"任务1");
我们在主线程中,往全局队列同步提交了Block,因为全局队列和主队列是两个队列,所以任务1的执行,并不需要等待任务0。所以等任务0结束,任务1也可以被执行。
当然这里因为提交Block所在队列,Block被执行的队列是完全不同的两个队列,所以这里用串行queue,也是不会死锁的,到这里我们也可以知道一些同步提交(dispatch_sync
)的阻塞机制:
同步提交Block,首先是阻塞的当前提交Block的线程,而在队列中,同步提交的Block,只会阻塞串行队列(由串行队列的同一时间只能执行一个任务的特性决定),并不会阻塞并发队列,当然dispatch_barrier
系列的除外,这个我会在后面的文章中讲到,欢迎大家继续关注我的博客
现在我们可以用一句话来总结产生死锁的原因就是:
使用同步方式(
dispatch_sync
)提交一个任务到一个串行队列时,如果提交这个任务的操作所处的线程,也是处于这个串行队列,就会引起死锁
开发中如何避免产生死锁
- 不要在主线程中使用同步方式添加任务到主队列
- 不要嵌套使用在自定义的串行队列中,嵌套使用同步方式添加任务到该串行队列
6种组合使用总结
总结 | 串行队列 | 并发队列 | 主队列 |
---|---|---|---|
同步添加(sync) | 不开辟新线程,在当前线程中串行执行任务 | 不开辟新线程,在当前线程中串行执行任务 | 死锁 |
异步添加(async) | 开辟新线程(1条),串行执行任务 | 开辟新线程(1/n条),并发执行任务 | 不开辟新线程,在主线程中顺序执行 |
线程间通信
#pragma mark - 子线程执行耗时代码,主线程更新UI
- (IBAction)threadInteraction:(UIButton *)sender {
NSLog(@"CurrentThread begin---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"threadInteraction---begin");
//异步添加任务到全局并发队列执行耗时操作
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//执行耗时任务
for (int i = 0; i < 2; ++i) {
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
}
//回到主线程更新UI
dispatch_sync(dispatch_get_main_queue(), ^{
//Do something here to update UI
});
});
NSLog(@"CurrentThread end---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"threadInteraction---end");
NSLog(@"*********************************************************");
}
执行结果如下:
2019-04-22 18:47:54.836027+0800 GCD(一) 队列、任务、串行、并发[13229:2669381] CurrentThread begin---<NSThread: 0x6000038e9400>{number = 1, name = main}
2019-04-22 18:47:54.836217+0800 GCD(一) 队列、任务、串行、并发[13229:2669381] threadInteraction---begin
2019-04-22 18:47:54.836436+0800 GCD(一) 队列、任务、串行、并发[13229:2669381] CurrentThread end---<NSThread: 0x6000038e9400>{number = 1, name = main}
2019-04-22 18:47:54.836619+0800 GCD(一) 队列、任务、串行、并发[13229:2669381] threadInteraction---end
2019-04-22 18:47:54.836761+0800 GCD(一) 队列、任务、串行、并发[13229:2669381] *********************************************************
2019-04-22 18:47:56.839416+0800 GCD(一) 队列、任务、串行、并发[13229:2669427] 1---<NSThread: 0x6000038b6d40>{number = 4, name = (null)}
2019-04-22 18:47:58.840646+0800 GCD(一) 队列、任务、串行、并发[13229:2669427] 1---<NSThread: 0x6000038b6d40>{number = 4, name = (null)}
在平常的开发中,我们最常用的就是提交一个任务到全局并发队列取执行一些比较耗时的操作(比如,文件下载、文件上传、图片解码、数据库操作、IO读写),然后再切回到主线程去更新UI,苹果官方限定了更新UI的操作只能在主线程中执行,所以,我们最后还是要回到主线程去处理我们的UI交互。
本文到这里已经基本结束,在接下来的文章中,我将会继续讲解GCD多线程编程的另外几个知识点,也是我们平时开发中实际很经常会用到的,如dispatch_barrier
、dispatch_group
、dispatch_semaphore
、线程安全
的相关内容
如果文中有错误的地方,或者与你的想法相悖的地方,请在评论区告知我,我会继续改进,如果你觉得这个篇文章总结的还不错,麻烦动动小手,给我的文章与Git代码样例点个✨