这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战
GCD简介
什么是GCD?全称是GrandCentralDispatch
纯C语⾔,提供了⾮常多强⼤的函数。总结为一句话:将任务添加到队列,并且指定执行任务的函数。
GCD的优势
GCD是苹果公司为多核的并⾏运算提出的解决⽅案
GCD会⾃动利⽤更多的CPU内核(⽐如双核、四核)
GCD会⾃动管理线程的⽣命周期(创建线程、调度任务、销毁线程)
程序员只需要告诉GCD想要执⾏什么任务,不需要编写任何线程管理代码
异步与同步
异步dispatch_async
- 不⽤等待当前语句执⾏完毕,就可以执⾏下⼀条语句
2. 会开启线程执⾏block的任务
3. 异步是多线程的代名词
同步dispatch_sync
- 必须等待当前语句执⾏完毕,才会执⾏下⼀条语句
串行队列和并发队列
队列遵循FIFO原则:先进先出
由于是FIFO,所以串行队列按顺序执行。并发队列只是调度任务并不是执行任务。
队列函数的应用
以下的函数执行顺序是怎样的
- (void)textDemo2{
// 同步队列
dispatch_queue_t queue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
// 异步函数
dispatch_async(queue, ^{
NSLog(@"2");
// 同步
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
复制代码
我们知道无论同步还是异步函数都是一个耗时任务。
再来一个,据说这个是新浪的面试题
- (void)wbinterDemo{
dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_SERIAL);
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
}
复制代码
正确答案是AC
分析:首先开启的是一个串行队列,12行的代码阻塞的是13行以下的,所以3在0之前,123没有顺序,789也没有顺序,使用排除法得到AC
死锁问题
如果把队列修改为串行队列那么此时调用的顺序为:
- (void)textDemo1{
dispatch_queue_t queue = dispatch_queue_create("cooci", NULL);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
复制代码
此时打印崩溃:
此时在这里,2执行完之后,执行到了一个代码块(dispatch_sync
而sync的特点是阻塞,必须等到自己执行完之后才可以),而队列由于是先进先出的原则,所以此时造成了4等待块执行完成,块的执行完成需要3执行,而3又等待4执行,这样就造成了一个死锁的问题。
改进:那么我们把4的任务删除,还会造成死锁嘛?答案是:还会死锁
观察调用栈发现死锁的函数是:_dispatch_sync_f_slow
实际上发生死锁的dispatch_async
和dispatch_sync
这两个代码块
GCD创建队列四种方式
// OS_dispatch_queue_serial 串行
dispatch_queue_t serial = dispatch_queue_create("hb", DISPATCH_QUEUE_SERIAL);
// OS_dispatch_queue_concurrent 并发
dispatch_queue_t conque = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
// DISPATCH_QUEUE_SERIAL max && 1
// queue 对象 alloc init class
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_queue_t globQueue = dispatch_get_global_queue(0, 0);
复制代码
我们知道dispatch_get_main_queue
是一个串行队列并且这个队列是在main()
调用之前主线程自动创建的,dispatch_get_global_queue
是一个并发队列。
打印输出可以得到这些信息:
主队列dispatch_get_main_queue
我们要想研究的话,可以从底层的源码入手。在项目中使用GCD的地方打个断点,查看调用栈,看看底层使用的是哪个库
由上面我们可以看出GCD在libdispatch.dylib
,接下来我们在openSource中去下载源码。
在源码中搜索这个dispatch_get_main_queue
可以定位到这里的代码
dispatch_get_main_queue(void)
{
return DISPATCH_GLOBAL_OBJECT(dispatch_queue_main_t, _dispatch_main_q);
}
复制代码
进入到DISPATCH_GLOBAL_OBJECT
,发现点击不进去我们只好全局搜索
#define DISPATCH_GLOBAL_OBJECT(type, object) ((type)&(object))
//dispatch_queue_main_t & _dispatch_main_q
复制代码
可以得出类型是dispatch_queue_main_t
,对象是_dispatch_main_q
,继续搜索
还有一个更简单的方式定位到这里,由上面我们知道,主队类的lable
= **com.apple.main-thread**
,所以我们可以直接在libdispatch
里面搜索也能够直接定位到这里。
回到上面的函数,我们发现主队列的赋值:
.dq_label = "com.apple.main-thread",
.dq_atomic_flags = DQF_THREAD_BOUND | DQF_WIDTH(1),
.dq_serialnum = 1,
复制代码
dq_serialnum
= 1就是串行队列嘛
那么我们在源码里面研究下串行队列的创建特性,就知道这个条件是不是必然条件了。那么队列是怎么创建的呢?我们知道这里用到了一个函数dispatch_queue_create
,那么就来研究下底层这个函数是怎么实现的吧,这样这些疑问就会马上明了
dispatch_queue_create
底层源码
第一个参数是const
类型,搜索的时候小技巧把它带上,可以快速定位
_dispatch_lane_create_with_target
直接定位到这个函数的返回值
DISPATCH_NOINLINE
static dispatch_queue_t
_dispatch_lane_create_with_target(const char *label, dispatch_queue_attr_t dqa,
dispatch_queue_t tq, bool legacy)
{
dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);
// 优先级的处理
// Step 1: Normalize arguments (qos, overcommit, tq)
// 初始化queue
// Step 2: Initialize the queue
// 申请和开辟内存
dispatch_lane_t dq = _dispatch_object_alloc(vtable,
sizeof(struct dispatch_lane_s));
// 构造函数初始化 dqai.dqai_concurrent:是否是并发
_dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ?
DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
(dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0));
dq->dq_label = label;
dq->dq_priority = _dispatch_priority_make((dispatch_qos_t)dqai.dqai_qos,
dqai.dqai_relpri);
if (overcommit == _dispatch_queue_attr_overcommit_enabled) {
dq->dq_priority |= DISPATCH_PRIORITY_FLAG_OVERCOMMIT;
}
if (!dqai.dqai_inactive) {
_dispatch_queue_priority_inherit_from_target(dq, tq);
_dispatch_lane_inherit_wlh_from_target(dq, tq);
}
_dispatch_retain(tq);
dq->do_targetq = tq;
_dispatch_object_debug(dq, "%s", __func__);
return _dispatch_trace_queue_create(dq)._dq;
}
复制代码
在函数构造初始化的时候有这么一行代码dqai.dqai_concurrent ? DISPATCH_QUEUE_WIDTH_MAX : 1
再进入_dispatch_queue_init
函数中去看下
static inline dispatch_queue_class_t
_dispatch_queue_init(dispatch_queue_class_t dqu, dispatch_queue_flags_t dqf,
uint16_t width, uint64_t initial_state_bits)
{
// ...
dqf |= DQF_WIDTH(width);
dq->dq_serialnum =
os_atomic_inc_orig(&_dispatch_queue_serial_numbers, relaxed);
// ...
return dqu;
}
复制代码
可以得出:
如果是并发队列dqf |= DQF_WIDTH(DISPATCH_QUEUE_WIDTH_MAX)
如果是串行队列dqf |= DQF_WIDTH(1)
继续研究dq->dq_serialnum
os_atomic_inc_orig(&_dispatch_queue_serial_numbers, relaxed)
// skip zero
// 1 - main_q 主队列
// 2 - mgr_q
// 3 - mgr_root_q
// 4,5,6,7,8,9,10,11,12,13,14,15 - global queues
// 17 - workloop_fallback_q
// we use 'xadd' on Intel, so the initial value == next assigned
#define DISPATCH_QUEUE_SERIAL_NUMBER_INIT 17
extern unsigned long volatile _dispatch_queue_serial_numbers;
复制代码
所以这里的_dispatch_queue_serial_numbers
只是代表的是创建的队列的归属(串行还是并发),所以上面的问题dq->dq_serialnum
= 1就是创建的主队列也是串行队列
扩展os_atomic_inc_orig
搜索os_atomic_inc_orig
发现是个宏定义
os_atomic_inc_orig(&_dispatch_queue_serial_numbers, relaxed)
os_atomic_add_orig((17), 1, relaxed)
_os_atomic_c11_op_orig((17), (v), relaxed, add, +)
atomic_fetch_add_explicit(_os_atomic_c11_atomic(17), v, memory_order_relaxedm)
atomic_fetch_add_explicit
是一个c++函数,也就是17 + 1
_dispatch_object_alloc
在creat的底层源码中,申请和开启内存使用的是这行代码
dispatch_lane_t dq = _dispatch_object_alloc(vtable,
sizeof(struct dispatch_lane_s));
复制代码
按照我们的常识,创建队列嘛,肯定使用 _dispatch_queue_alloc
这个函数,但是这里为什么使用的是_dispatch_object_alloc
并且用dispatch_lane_t
来接收?预知后续,我们下一篇来讲解。