一、引言
前边一篇文章我们已经大致介绍了GCD的有些概念和函数的执行。接下来让我们继续带着探索的心里去学习,继续前行,继续介绍线程是如何开辟和创建的,又是什么时候去执行相关的调度任务。接下来让我们从相关的概念入手
二 、线程和任务
2.1 GCD的优势
- 1 GCD 是苹果公司为多核的并行运算提出的解决方案
- 2 GCD 会自动利用更多的CPU内核(比如双核、四核)
- 3 GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
- 4 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码
2.2 任务
- 1 任务使用 block 封装
- 2 任务的 block 没有参数也没有返回值
2.3 同步任务
- 1 函数:
dispatch_sync
- 2 必须等待当前语句执行完毕,才会执行下一条语句
- 3 不会开启线程
- 4 在当前执行 block 的任务
2.4异步任务
- 1 异步
dispatch_async
- 2 不用等待当前语句执行完毕,就可以执行下一条语句
- 3 会开启线程执行 block 的任务
- 4 异步是多线程的代名词
2.5主队列
- 1 函数:
dispatch_get_main_queue()
; - 2 特殊的串行队列
- 3专门用来在主线程上调度任务的队列
- 4不会开启线程
- 5 如果当前主线程正在执行任务,那么无论主队列中当前被添加了什么任务,都不会被调度
2.6.全局队列
- 1 函数:
dispatch_get_global_queue(0,0)
; - 2 为了方便程序员使用,苹果提供了全局队列
- 3全局队列是一个并发队列
- 4 在使用多线程开发时,如果对队列没有特殊需求,在执行异步任务时,可以直接使用全局队列
2.7 队列和任务搭配
- 1 同步串行队列:不开启新线程,串行执行任务;
- 2 同步并发队列:不开启新线程,串行执行任务;
- 3同步主队列:如果在主线程调用,造成死锁;如果在其他线程调用,不开启新线程,串行执行,在主线程串行执行任务;
- 4 异步串行队列:开启新线程,串行执行任务;
- 5 异步并发队列:开启新线程,并发执行任务;
- 6 异步主队列:不开启新线程,在主线程串行执行任务
- 7 死锁:
三、GCD 底层源码分析
3.1 dispatch_async(异步函数)在何时开辟线程
我们都知道主线程和同步线程不会开辟子线程,但是当我们用dispatch_async
(异步函数)的时候,线程是如何开辟的呢?是何时执行的调度任务呢?这些都是我们在使用GCD的情况下很少去考虑的问题,为了弄清楚相关的原理,让我们详细去研究一下相关的流程。
我们在测试Demo中打印一个简单的语句,并且进行相应的断点调试,然后打印出相应的堆栈信息
dispatch_async(_queue, ^{
NSLog(@"1111111");
})
在控制台进行bt
打印所有的堆栈信息,可以得出相应的执行步骤如下
从打印结果看我们知道异步线程会执行_dispatch_lane_invoke
,有兴趣的同学可以加一个符号断点进行调试,程序一定会执行到此为止的,我们进入相应的定义
void
_dispatch_lane_invoke(dispatch_lane_t dq, dispatch_invoke_context_t dic,
dispatch_invoke_flags_t flags)
{
_dispatch_queue_class_invoke(dq, dic, flags, 0, _dispatch_lane_invoke2);
}
然后到_dispatch_lane_serial_drain
的定义
dispatch_queue_wakeup_target_t
_dispatch_lane_serial_drain(dispatch_lane_class_t dqu,
dispatch_invoke_context_t dic, dispatch_invoke_flags_t flags,
uint64_t *owned)
{
flags &= ~(dispatch_invoke_flags_t)DISPATCH_INVOKE_REDIRECTING_DRAIN;
return _dispatch_lane_drain(dqu._dl, dic, flags, owned, true);
}
最终会走到_dispatch_root_queue_poke_slow
方法,进行相应的线程创建,
其中创建线程的代码如下
#if !defined(_WIN32)
pthread_attr_t *attr = &pqc->dpq_thread_attr;
pthread_t tid, *pthr = &tid;
#if DISPATCH_USE_MGR_THREAD && DISPATCH_USE_PTHREAD_ROOT_QUEUES
if (unlikely(dq == &_dispatch_mgr_root_queue)) {
pthr = _dispatch_mgr_root_queue_init();
}
#endif
do {
_dispatch_retain(dq); // released in _dispatch_worker_thread
while ((r = pthread_create(pthr, attr, _dispatch_worker_thread, dq))) {
if (r != EAGAIN) {
(void)dispatch_assume_zero(r);
}
_dispatch_temporary_resource_shortage();
}
} while (--remaining);
#else // defined(_WIN32)
#if DISPATCH_USE_MGR_THREAD && DISPATCH_USE_PTHREAD_ROOT_QUEUES
if (unlikely(dq == &_dispatch_mgr_root_queue)) {
_dispatch_mgr_root_queue_init();
}
从最终的线程创建我们知道,线程创建过程当中,不断的判断需要创建多少线程,每创建一个就减少一个,从而调用pthread_create
的API进行创建。这就是GCD
线程创建的过程;
3.2 dispatch_barrier_async(栅栏函数)
dispatch_barrier_async
(栅栏函数)他,通过其命名我们就知道是拦截的意思。也就是在栅栏函数之前的任务执行完成后,才能执行后边的任务。先看一下官方文档
dispatch_barrier_sync
: Submits a barrier block object for execution and waits until that block completes.(提交一个栅栏函数在执行中,它会等待栅栏函数执行完)
dispatch_barrier_async
: Submits a barrier block for asynchronous execution and returns immediately.(提交一个栅栏函数在异步执行中,它会立马返回)
作者理解:dispatch_barrier_sync
需要等待栅栏执行完才会执行栅栏后面的任务,而dispatch_barrier_async
无需等待栅栏执行完,会继续往下走(保留在队列里)
原因:在同步栅栏时栅栏函数在主线程中执行,而异步栅栏中开辟了子线程栅栏函数在子线程中执行
我们看看dispatch_barrier_async
异步栅栏函数的执行结果
dispatch_queue_t concurrentQueue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
sleep(1);
NSLog(@"123");
});
/* 2. 栅栏函数 */ // - dispatch_barrier_sync
dispatch_barrier_async(concurrentQueue, ^{
NSLog(@"---------------------%@------------------------",[NSThread currentThread]);
});
/* 3. 异步函数 */
dispatch_async(concurrentQueue, ^{
NSLog(@"加载那么多,喘口气!!!");
});
NSLog(@"**********起来干!!");
执行结果是
我们再次看看dispatch_barrier_sync
同步栅栏函数的执行结果:
dispatch_queue_t concurrentQueue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
sleep(1);
NSLog(@"123");
});
/* 2. 栅栏函数 */ // - dispatch_barrier_sync
dispatch_barrier_sync(concurrentQueue, ^{
NSLog(@"---------------------%@------------------------",[NSThread currentThread]);
});
/* 3. 异步函数 */
dispatch_async(concurrentQueue, ^{
NSLog(@"加载那么多,喘口气!!!");
});
NSLog(@"**********起来干!!");
打印结果如下
结论 dispatch_barrier_sync
需要等待栅栏执行完才会执行栅栏后面的任务,而dispatch_barrier_async
无需等待栅栏执行完,会继续往下走(保留在队列里)
3.3 dispatch_semaphore_t(信号量)
信号量是通过信号的数量来控制相应的任务调度,需要设置一个固定的信号量,有可以操作的任务调度才可以执行,就好比停车场的空车位数一样,数量一定,如果有空就可以进行停车,如果车位已满就让后来的人等待。其中信号量控制有三个非常重要的函数
dispatch_semaphore_create
dispatch_semaphore_wait
dispatch_semaphore_signal
1
dispatch_semaphore_create(long value)
;和GCD的group等用法一致,这个函数是创建一个dispatch_semaphore_类型的信号量,并且创建的时候需要指定信号量的大小。2
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
; 等待信号量。该函数会对信号量进行减1操作。如果减1后信号量小于0(即减1前信号量值为0),那么该函数就会一直等待,也就是不返回(相当于阻塞当前线程),直到该函数等待的信号量的值大于等于1,该函数会对信号量的值进行减1操作,然后返回。3
dispatch_semaphore_signal(dispatch_semaphore_t deem)
; 发送信号量。该函数会对信号量的值进行加1操作。
通常等待信号量和发送信号量的函数是成对出现的。并发执行任务时候,在当前任务执行之前,用dispatch_semaphore_wait
函数进行等待(阻塞),直到上一个任务执行完毕后且通过dispatch_semaphore_signal
函数发送信号量(使信号量的值加1),dispatch_semaphore_wait
函数收到信号量之后判断信号量的值大于等于1,会再对信号量的值减1,然后当前任务可以执行,执行完毕当前任务后,再通过dispatch_semaphore_signal函数发送信号量(使信号量的值加1),通知执行下一个任务......如此一来,通过信号量,就达到了并发队列中的任务同步执行的要求。
dispatch_semaphore_signal底层实现过程
dispatch_semaphore_signal
的内部实现是
dispatch_semaphore_signal(dispatch_semaphore_t dsema)
{
long value = os_atomic_inc2o(dsema, dsema_value, release);
if (likely(value > 0)) {
return 0;
}
if (unlikely(value == LONG_MIN)) {
DISPATCH_CLIENT_CRASH(value,
"Unbalanced call to dispatch_semaphore_signal()");
}
return _dispatch_semaphore_signal_slow(dsema);
}
到 os_atomic_inc2o(dsema, dsema_value, release);
起底层定义是
#define os_atomic_inc2o(p, f, m) \
os_atomic_add2o(p, f, 1, m)
也就是调用了os_atomic_add2o
的函数封装;其内部实现如下
#define os_atomic_add2o(p, f, v, m) \
os_atomic_add(&(p)->f, (v), m)
也就是os_atomic_add
其内部定义封装是
#define os_atomic_add(p, v, m) \
_os_atomic_c11_op((p), (v), m, add, +)
再次到_os_atomic_c11_op
函数的实现,定义如下
最终我们知道其内部是调用了atomic_fetch_##o##_explicit(_os_atomic_c11_atomic(p)
而其中##o##
是形参,替换成第三个参数就是add
得到的结果就是atomic_fetch_add_explicit
,我们百度搜索就知道
原子替换指向的值
obj
和添加arg
到旧值的结果obj
,并返回obj先前保存的值。操作是读取 - 修改 - 写入操作。第一个版本根据命令对内存进行访问memory_order_seq_cst
,第二个版本根据内存访问内存访问order
具体链接
同理,dispatch_semaphore_wait
也是一样的原理过程。这就是信号量的底层实现逻辑。
3.4 dispatch_once_t(单利)的底层实现和逻辑
单利的底层实现代码定义如下
dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
dispatch_once_gate_t l = (dispatch_once_gate_t)val;
#if !DISPATCH_ONCE_INLINE_FASTPATH || DISPATCH_ONCE_USE_QUIESCENT_COUNTER
uintptr_t v = os_atomic_load(&l->dgo_once, acquire);
if (likely(v == DLOCK_ONCE_DONE)) {
return;
}
#if DISPATCH_ONCE_USE_QUIESCENT_COUNTER
if (likely(DISPATCH_ONCE_IS_GEN(v))) {
return _dispatch_once_mark_done_if_quiesced(l, v);
}
#endif
#endif
if (_dispatch_once_gate_tryenter(l)) {
return _dispatch_once_callout(l, ctxt, func);
}
return _dispatch_once_wait(l);
}
单利的保证线程安全过程
根据以上代码可知,
- 1 我们可以知道外部传入的一个任务值
val
,单利内部会对这个任务值进行一个处理,处理成一个dispatch_once_gate_t
对象l
, - 2 通过内核加载过程,看看当前任务十分已经被标记过是需要执行的任务;
- 3 如果是被标记过的任务就直接执行当前任务,并且对当前任务的执行流程加入一个锁操作;
- 4 如果没有标记过,就对改任务进行标记,并且返回一个标识;
- 5 最后通过
CPU
调度,选择已经标识过的任务进行执行_dispatch_once_callout
- 6 同时有多个任务来就需要等待当前任务的处理,等到处理过后进行标识才能从新调用;
四、总结
以上就是GCD相关函数的底层实现过程以及一些原理的介绍,仅仅凭借个人的学习经验和代码阅读过程以及Demo的调试得到的感触,所以很多东西只是个人的理解,如果大神们有更好的思路和意见,欢迎多多指正。我一定虚心学习,追求大神们的步伐。