前言
作为一名iOS开发者,我们都知道在iOS中常用的多线程管理方式有三种,NSThread、GCD和NSOperation。三者的对比如下:
- NSThread提供了创建线程、调度线程、销毁线程等接口,但是接口比较复杂,需要程序员自己管理线程的生命周期,所以更多是用在调试时。
- NSOperation是基于GCD的OC封装,与GCD很类似,NSOperation有队列和操作两个概念,两者配合实现多线程。
- GCD,全称Grand Central Dispatch,是苹果提供的多线程管理方案,会自动利用更多的CPU内核,并且可以自动管理线程的生命周期,例如创建线程、调度线程及销毁线程,这些都不用程序员操心,程序员只需要告诉GCD想要执行什么任务,而不用写任何线程管理代码。
本篇就来初探下GCD的队列与函数。
一、队列与函数
在探索队列和函数之前,先看一个demo,代码如下:
如图所示,为一个GCD的简单调用,在这个demo中,创建了一个 打印执行任务 的任务,同时创建了一个串型队列,并将任务添加到队列中,由一个异步函数执行。这一过程可以概括为将任务添加到队列,并且指定执行任务的函数。
在GCD中,任务被封装成了一个个的block,即dispatch_block_t,我们可以将要执行的代码放在这样的block中。
1.1 队列
GCD还提供了一个函数 dispatch_queue_create,代码定义如下:
/*!
* **@function** dispatch_queue_create
*
* **@abstract**
* Creates a new dispatch queue to which blocks may be submitted.
* 创建一个可以提交代码块的新队列
* **@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_SERIAL 和 DISPATCH_QUEUE_CONCURRENT标识。
串行队列和并发队列的区别如下图:
- 串行队列中任务只能串行调度,任务依次执行
- 并发队列可以在同一时间段内,同时调度多个任务,具备并发能力
1.2 函数
然后将创建的队列和任务作为参数传递到执行函数中,GCD提供了两种函数 dispatch_async 和 dispatch_sync,分别表示异步和同步函数。
同步函数和异步函数的区别如下图:
如图所示,假定任务3是一个耗时任务,当采用同步函数时,执行到任务3,就会阻塞后续的任务执行,后续任务必须等到任务3执行完毕后才能得以执行,俗称“护犊子”;而如果采用异步函数,执行到任务3时,后续任务会继续执行,不会被阻塞。
以一个demo对比验证如下:
对比可以看出,采用异步函数时, task2中的sleep不会导致task3的阻塞,而采用同步函数时,task3必须等待task2执行完毕,由此可以验证上面的结论。并且从结果中可以发现,异步函数拥有开启新的线程的能力,而同步函数不会开启新线程。
二、函数与队列的组合
在上一小节中,我们知道GCD中函数有两种 同步和异步,队列分为两种 串行和并发,加上两种特殊的队列 主队列和全局并发队列,所以可以有如下几种组合:
- 同步函数 + 主队列
- 同步函数 + 全局并发队列
- 同步函数 + 普通串行队列
- 同步函数 + 普通并发队列
- 异步函数 + 主队列
- 异步函数 + 全局并发队列
- 异步函数 + 普通串行队列
- 异步函数 + 普通并发队列
其实主队列是一种特殊的串行队列,全局并发队列是一种特殊的并发队列,不过在某些情况下与普通队列有所不同,我们分别通过demo来看下这几种组合会有什么效果。
2.1 同步函数 + 主队列
同步函数 + 主队列的代码如下:
- (void)syncMain {
dispatch_queue_t mainQueue = dispatch_get_main_queue();
NSLog(@"1 ---- %@",[NSThread currentThread]);
dispatch_sync(mainQueue, ^{
NSLog(@"2 ---- %@",[NSThread currentThread]);
});
NSLog(@"3 ---- %@",[NSThread currentThread]);
}
复制代码
代码执行结果如下:
可以看到同步函数 + 主队列的执行结果是发生了死锁,_dispatch_sync_f_slow () 是发生死锁时GCD调用的函数。发生死锁的原因如下:
2.2 异步函数 + 主队列
异步函数 + 主队列的代码如下:
- (void)asyncMain {
dispatch_queue_t mainQueue = dispatch_get_main_queue();
NSLog(@"1 ---- %@",[NSThread currentThread]);
dispatch_async(mainQueue, ^{
NSLog(@"2 ---- %@",[NSThread currentThread]);
});
NSLog(@"3 ---- %@",[NSThread currentThread]);
}
复制代码
代码执行结果如下:
从执行结果可以发现,使用异步函数没有阻塞后面的任务,因此也不会发生死锁。并且可以发现,在主队列下使用异步函数也不会开启新的线程。
2.3 同步函数 + 全局并发队列
同步函数 + 全局并发队列的代码如下:
- (void)syncGlobal {
dispatch_queue_t global = dispatch_get_global_queue(0, 0);
NSLog(@"1 ---- %@",[NSThread currentThread]);
dispatch_sync(global, ^{
NSLog(@"2 ---- %@",[NSThread currentThread]);
dispatch_sync(global, ^{
NSLog(@"3 ---- %@",[NSThread currentThread]);
});
NSLog(@"4 ---- %@",[NSThread currentThread]);
});
NSLog(@"5 ---- %@",[NSThread currentThread]);
}
复制代码
这次代码稍微复杂一些,内部再嵌套了一个sync函数,其执行结果如下:
通过结果可以发现,在全局并发队列下,使用sync也不会阻塞,更不会死锁。并且可以发现同步函数不会开启新的线程,因此虽然是在全局并发队列中,但是依然是在主线程。
2.4 异步函数 + 全局并发队列
同步函数 + 全局并发队列的代码如下:
- (void)syncGlobal {
dispatch_queue_t global = dispatch_get_global_queue(0, 0);
NSLog(@"1 ---- %@",[NSThread currentThread]);
dispatch_async(global, ^{
NSLog(@"2 ---- %@",[NSThread currentThread]);
dispatch_async(global, ^{
NSLog(@"3 ---- %@",[NSThread currentThread]);
});
NSLog(@"4 ---- %@",[NSThread currentThread]);
});
NSLog(@"5 ---- %@",[NSThread currentThread]);
}
复制代码
代码执行结果如下:
可以发现代码是异步执行,不会阻塞,也不会死锁,并且async函数会开启新的线程6和7。
2.5 同步函数 + 普通串行队列
同步函数 + 普通串行队列的代码,我们分两部分看,先看第一部分如下
- (void)syncSerial {
dispatch_queue_t serial = dispatch_queue_create("BPTest.sync.Serial", DISPATCH_QUEUE_SERIAL);
NSLog(@"1 ---- %@",[NSThread currentThread]);
dispatch_sync(serial, ^{
NSLog(@"2 ---- %@",[NSThread currentThread]);
// dispatch_sync(serial, ^{
// NSLog(@"3 ---- %@",[NSThread currentThread]);
// });
// NSLog(@"4 ---- %@",[NSThread currentThread]);
});
NSLog(@"3 ---- %@",[NSThread currentThread]);
}
复制代码
执行结果如下:
可以发现此处并未发生死锁,对比同步函数 + 主队列,同样都是串行队列,为何主队列会死锁闪退,而普通的串行队列可以正常运行呢?其原因如下图:
同步函数 + 主队列中,因为只有一个主队列,所以会发生死锁,而在同步函数 + 串行队列中,因为有除了有一个串行队列外,还有个默认的主队列,我们的次任务是添加到串行队列中的,因此不会死锁闪退。
那么同步函数 + 串行队列一定是安全的吗?我们接着看下面的代码:
可以发现,同步函数 + 串行队列一样会发生闪退,那么我们分析下这次死锁闪退的原因,如下图所示:
其实原因与主队列闪退是一致的,本次所添加的任务都是添加到我们创建的串行队列中,所以会发生和主队列一样死锁闪退。
2.6 异步函数 + 普通串行队列
异步函数 + 普通串行队列的代码如下:
- (void)asyncSerial {
dispatch_queue_t serial = dispatch_queue_create("BPTest.async.Serial", DISPATCH_QUEUE_SERIAL);
NSLog(@"1 ---- %@",[NSThread currentThread]);
dispatch_async(serial, ^{
NSLog(@"2 ---- %@",[NSThread currentThread]);
dispatch_async(serial, ^{
NSLog(@"3 ---- %@",[NSThread currentThread]);
});
NSLog(@"4 ---- %@",[NSThread currentThread]);
});
NSLog(@"5 ---- %@",[NSThread currentThread]);
}
复制代码
代码执行结果如下:
虽然还是添加在串行队列中,但是因为使用的是异步函数,不会发生阻塞,所以也不会产生死锁。
2.7 同步函数 + 普通并发队列
同步函数 + 普通并发队列的代码如下:
- (void)syncConcurrent {
dispatch_queue_t concurrent = dispatch_queue_create("BPTest.sync.concurrent", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1 ---- %@",[NSThread currentThread]);
dispatch_sync(concurrent, ^{
NSLog(@"2 ---- %@",[NSThread currentThread]);
dispatch_sync(concurrent, ^{
NSLog(@"3 ---- %@",[NSThread currentThread]);
});
NSLog(@"4 ---- %@",[NSThread currentThread]);
});
NSLog(@"5 ---- %@",[NSThread currentThread]);
}
复制代码
执行结果如下:
与同步函数 + 全局并发队列的情况一致,也不会发生死锁闪退,这里分析下为何使用并发队列没有闪退,而用串行队列闪退了,分析如图:
2.8 异步函数 + 普通并发队列
代码及执行结果如下:
这里既不会阻塞,也不会死锁。
总结
本篇主要介绍了GCD的队列和函数,可以得到以下几个结论:
- 1、函数分为同步函数和异步函数,函数控制是否有开辟线程的能力,同步函数不具有开启新线程的能力,异步函数具有开辟新线程的能力,但是会根据实际情况确定是否开辟新线程
- 2、队列主要分为串行队列和并发队列,队列决定了线程的调度能力,串行队列只能调度一个线程,因此任务只能顺序执行,并发队列则具有并发调度的能力。
- 队列和函数的组合有以下几个结果:
同步 | 异步 | |
---|---|---|
主队列 | 死锁闪退 | 正常,但不会开辟新线程(主队列中只有主线程) |
全局并发队列 | 正常,同步不开辟新线程 | 正常,开辟新线程 |
普通串行队列 | 部分情况下会死锁闪退 | 正常,会开辟新线程 |
普通并发列 | 正常,同步函数不开启新线程 | 正常,会开辟新线程 |
以上即为对于GCD队列与函数的初步探索,后续会继续探索GCD源码,从中进一步学习。本篇探索到此为止,如果有不准确的地方,欢迎大家指正。