多线程 - IOS GCD(1)
前言
一、GCD任务函数的定义和特点
二、GCD队列的定义和特点
三、GCD队列和函数的组合使用
四、队列和函数的耗能问题
五、主线程和主队列
六、基础面试题分析
GCD全称是Grand Central Dispatch,是纯C语言开发,提供了很多强大的函数。GCD是苹果公司为多核的并行运算提出的解决方案,它会自动利用更多的CPU内核(多核) ,自动管理线程的生命周期(创建线程、调度任务、销毁线程),功能很强大。今天就来探索总结一下。
GCD 最新源码下载,后面的分析都改为libdispatch-1271.40.12版本的源码。
一、GCD任务函数的定义和特点
GCD 简单的来说就是“将任务添加到队列,并且指定执行任务的函数”。
那么先来看下什么是任务,什么是函数。
/// 同步函数
- (void)syncTest {
dispatch_block_t tasks = ^{
NSLog(@"执行同步任务");
};
dispatch_sync(dispatch_get_global_queue(0, 0), taks);
}
/// 异步函数
- (void)asyncTest {
dispatch_block_t tasks = ^{
NSLog(@"执行异步任务");
};
dispatch_async(dispatch_get_global_queue(0, 0), taks);
}
如以上代码,可以总结如下:
1、GCD的任务使用block封装。
2、任务的block没有参数,没有返回值。
3、执行任务的函数有同步函数“dispatch_sync”和异步函数"dispatch_async"。
关于同步函数的特点是:
(1)必须等待当前语句执行完毕,才会执行下一条语句
(2)不会开启线程
关于异步函数的特点是:
(1)不用等待当前语句执行完毕,就可以执行下一条语句
例如:在主队列异步执行任务
- (void)asyncTest2 {
dispatch_async(dispatch_get_main_queue(), ^{
sleep(1);
NSLog(@"1");
});
NSLog(@"2");
}
打印结果为:2 1
因为异步函数不用等待当前语句执行完毕,所以先打印2,然后1会在runloop下一次循环的时候打印。
(2)会开启线程执行block任务(除主队列以外)
二、GCD队列的定义和特点
GCD的队列分为2类:串行队列和并发队列。
1、串行队列

简单的来说就是任务一个接一个的执行。
其中主队列(dispatch_get_main_queue())是一种特殊的串行队列,它的特点如下:
(1)专门用来在主线程上调度任务的串行队列
(2)不会开启线程(无论是同步函数还是异步函数执行任务时)
(3)如果当前主线程正在有任务执行,那么无论主队列中当前被添加了什么任务,都不会被调度。
* Returns the main queue. This queue is created automatically on behalf of
* the main thread before main() is called.
通过注释可以看到,主队列是在main()之前主动创建的。
2、并发队列

这里再复习一下并行和并发的区别:
(1)并行是同一时间,有不同的线程同时在执行任务。比如多核CPU,同一时间有多个线程同时执行任务。
(2)并发是单位时间内,一条线程执行多个任务。并发一定要以时间为单位。比如时间片轮转机制就是并发。
关于GCD的并发队列而言,可能是多个不同线程执行,也可能是单条线程时间片轮转。这取决与系统底层线程池的调度。所以这里就不区分并发和并行的概念了,统一叫做并发队列。
其中全局队列(dispatch_get_global_queue(0,0))是一种特殊的并发队列。
注意:栅栏函数不能执行全局队列的任务,因为全局队列很多地方在用,包括系统都在用。
三、GCD队列和函数的组合使用
1、同步函数+串行队列
(1)不会开启线程,在当前线程执行任务
(2)任务一个接着一个执行
(3)会产生堵塞
2、同步函数+并发队列
(1)不会开启线程,在当前线程执行任务
(2)任务一个接着一个执行
3、异步函数+串行队列
(1)开启一条新线程
(2)任务一个接一个执行
4、异步函数+并发队列
(1)开启一条或多条线程
(2)任务异步执行,没有顺序,取决于cpu调度
四、队列和函数的耗能问题
当创建一个队列,或者执行函数的时候,会有一定的时间损耗
///耗能问题
- (void)timeConsuming {
//当前时间
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
dispatch_queue_t queue = dispatch_queue_create("com.xxx.cn", DISPATCH_QUEUE_SERIAL);
NSLog(@"%f",CFAbsoluteTimeGetCurrent()-time);
}
多次结果打印 0.000001 - 0.000003
///耗能问题
- (void)timeConsuming {
//当前时间
CFAbsoluteTime time = CFAbsoluteTimeGetCurrent();
dispatch_queue_t queue = dispatch_queue_create("com.xxx.cn", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
//....
});
/*
dispatch_async(queue, ^{
//....
});*/
NSLog(@"%f",CFAbsoluteTimeGetCurrent()-time);
}
多次结果打印 0.000004 - 0.000005
证明在创建队列,和执行同步、异步任务的时候,是有一定的时间损耗的。
五、主线程和主队列
在理解主线程和主队列之前,首先要明确一点:任务是在线程上执行的,不是在队列执行的。队列是一个遵循FIFO的数据结构,它会合理的安排任务执行的顺序。
1、主线程只执行主队列的任务吗?
主线程运行在一个runloop中,它不仅只执行主队列的任务,还可以执行其他队列的任务。
例如:
dispatch_queue_t queue = dispatch_queue_create("xxxx", nil);
dispatch_sync(queue, ^{
NSLog(@"\ncurrent thread = %@\ncurren queue = %@\nmain queue = %@\n",
[NSThread currentThread],
[NSString stringWithCString:dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) encoding:NSUTF8StringEncoding],
[NSString stringWithCString:dispatch_queue_get_label(dispatch_get_main_queue()) encoding:NSUTF8StringEncoding]);
});
打印结果为:
current thread = <_NSMainThread: 0x600003d207c0>{number = 1, name = main}
curren queue = xxxx
main queue = com.apple.main-thread
结论:主线程不仅能执行主队列的任务,还可以执行其他队列的任务。
2、主队列的任务一定是在主线程执行的吗?
通过搜索很多资料以及博客,只找到了一种“主队列的任务不在主线程执行”情况。
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"\ncurrent thread = %@\ncurren queue = %@\nmain queue = %@\n",
[NSThread currentThread],
[NSString stringWithCString:dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) encoding:NSUTF8StringEncoding],
[NSString stringWithCString:dispatch_queue_get_label(dispatch_get_main_queue()) encoding:NSUTF8StringEncoding]);
});
});
NSLog(@"======");
dispatch_main();
打印结果为:
current thread = <NSThread: 0x600000974100>{number = 7, name = (null)}
curren queue = com.apple.main-thread
main queue = com.apple.main-thread
这种情况是因为调用了dispatch_main();
dispatch_main()是伪装的pthread_exit():它将主队列变成普通的串行调度队列,然后终止主线程。
void dispatch_main(void) {
_dispatch_root_queues_init();
if (pthread_main_np()) {
_dispatch_object_debug(&_dispatch_main_q, "%s", __func__);
_dispatch_program_is_probably_callback_driven = true;
_dispatch_ktrace0(ARIADNE_ENTER_DISPATCH_MAIN_CODE);
pthread_exit(NULL);
}
DISPATCH_CLIENT_CRASH(0, "dispatch_main() must be called on the main thread");
}
结论:在通常情况下,主队列的任务一定是在主线程执行的。
3、更加安全的UI操作
通过网上资料,会有UI操作在主线程但是不在主队列的bug:"在 ReactiveCocoa 的一个 issue里提到在MapKit 中的 MKMapView 有个 addOverlay 方法,这个方法不仅要在主线程中执行,而且要把这个操作加到主队列中才可以"。
所以我们如果要更加安全的去操作UI,应该保证UI的操作既在主线程中又在主队列中。那么通常情况下,在主队列中的任务一定在主线程中执行,所以我们可以使用SDWebImage中的一个宏来实现安全的函数:
#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
#endif
六、基础面试题分析
1、面试1:
- (void)testDemo1 {
dispatch_queue_t queue = dispatch_queue_create("xxx", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
// 耗时
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_async(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
打印结果为:1 5 2 4 3
解释:因为dispatch_async会有一定时间损耗,所以通常情况下(除主队列阻塞)会先执行5,然后再执行2。
2、面试2:
- (void)testDemo2 {
//同步队列
dispatch_queue_t queue = dispatch_queue_create("xxx", NULL);
NSLog(@"1");
// 异步函数
dispatch_async(queue, ^{
NSLog(@"2");
// 同步
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
打印结果为 1 5 2 死锁。
解释:
(1)首先会打印 1 5 2,这里毫无疑问。
(2)当任务执行到dispatch_sync时,同步函数会把3的打印追加到串行队列的队尾,由于FIFO的原则,如果要执行打印3,那么必须先执行完dispatch_sync函数。但是要执行完dispatch_sync函数又需要先执行完打印3,此时形成了互相等待,死锁了。
3、面试3:
dispatch_queue_t queue = dispatch_queue_create("xxx", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{ // 耗时
NSLog(@"1");
});
dispatch_async(queue, ^{
NSLog(@"2");
});
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"0");
dispatch_async(queue, ^{
NSLog(@"7");
});
dispatch_async(queue, ^{
NSLog(@"8");
});
dispatch_async(queue, ^{
NSLog(@"9");
});
// A: 1230789
// B: 1237890
// C: 3120798
// D: 2137890
答案是A和C。
解释:因为dispatch_sync同步函数会阻塞,重点是它阻塞的是整个同步函数下面的任务,所以1、2、3是没有顺序的,然后会执行0,最后7 8 9 也是没有顺序的。所以答案是A和C。
面试4:
- (void)testDemo4 {
dispatch_queue_t queue = dispatch_queue_create("xxx", NULL);
dispatch_async(queue, ^{
NSLog(@"1");
});
dispatch_async(queue, ^{
NSLog(@"2");
});
dispatch_sync(queue, ^{
NSLog(@"3");
});
dispatch_async(queue, ^{
sleep(1);
NSLog(@"4");
});
dispatch_async(queue, ^{
NSLog(@"5");
});
dispatch_async(queue, ^{
NSLog(@"6");
});
}
答案是:1 2 3 4 5 6
解释:因为串行队列中,都是按照顺序执行的。
面试5:
- (void)testDemo5 {
dispatch_queue_t queue1 = dispatch_queue_create("xxx", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue1, ^{
NSLog(@"1");
});
dispatch_async(queue1, ^{
NSLog(@"2");
});
dispatch_barrier_async(queue1, ^{
NSLog(@"3");
});
dispatch_async(queue1, ^{
NSLog(@"4");
});
}
答案是:1 2 3 4 或 2 1 3 4
解释:dispatch_barrier_async会在当前队列中阻塞,直到当前任务完成之后,才能执行下面的任务。
面试6
队列排到的任务一定先执行吗?
队列排到的任务不一定先执行,因为虽然排到了,但是任务的执行取决于线程,是线程在执行任务,而线程的调度又取决于cpu,所以队列排到的任务不一定先执行。