这是我参与8月更文挑战的第22天,活动详情查看:8月更文挑战
1. 队列和线程
一个线程中可以有多个队列,每个队列中可以执行多个任务,队列可以对任务进行排序,队列依靠线程来执行任务。
1.1 主队列
主队列(Main queue)
是与主线程关联的调度队列,是一种串行队列(Serial)
并且在main函数之前
就已经被创建了,与UI相关
的操作必须放在Main queue中执行。用dispatch_get_main_queue()
可以获取到主队列。
在main函数之前打下断点,看到已经有了主队列,并且是串行的。那么意味着主队列可能是在dyld之后,main函数之前创建的。
接下来去libdispatch
源码里面查找主队列是否有初始化。

复制代码
接下来搜索DISPATCH_GLOBAL_OBJECT
。看到这里第一个是类型,而第二个才是真正的对象。那么也说明了之前传的参数dispatch_queue_main_t是类型,_dispatch_main_q才是对象
。
接下来搜索_dispatch_main_q,看到其在这里赋值了。
除了这种方式还可以通过搜索线程的label
来寻找。这里打个断点bt一下,得到主线程的label,然后搜索。
直接就搜索到了主队列。这里.dq_serialnum = 1
不是决定队列是不是串行队列的地方,这里的DQF_WIDTH(1)
才是证明了主队列是串行队列的地方。
一般可以通过dispatch_queue_create
来创建队列,那么在源码中搜索dispatch_queue_create这个函数的实现。
接下来搜索_dispatch_lane_create_with_target
。看到这里有一百多行代码,就来看返回值。这里返回值有一个重要的东西就是dq,看到dq是dispatch_lane_t
类型,并且做了开辟内存以及初始化
。并且这里第三个参数做了判断,如果是concurrent则传DISPATCH_QUEUE_WIDTH_MAX
,否则就传1进去。
接下来搜索_dispatch_queue_init
函数,这里看到第三个参数用在了dqf |= DQF_WIDTH(width)
这里面。这里也就证明了DQF_WIDTH(width)是确定一个队列是并行还是串行队列
的地方。
那么dq_serialnum是代表什么呢?
搜索_dispatch_queue_serial_numbers
,这里看到其赋值的地方。
接下来就搜索DISPATCH_QUEUE_SERIAL_NUMBER_INIT
。看到了这样的注释。所以.dq_serialnum = 1代表的是主队列。
回到_dispatch_lane_create_with_target
往上看,看到这里是在初始化参数
,然后初始化队列
。
1.2 Global queue (全局队列)
Global queue (全局队列)
运行在后台线程
,是系统内共享的全局队列
,是一种并行队列(Concurrent)
。
可以通过 dispatch_get_global_queue(0, 0)
获取全局队列。
获取到Global queue 的label,然后到源码中搜索。
在源码中搜索到的Global queue。
源码往上看,看到其还是一个数组
。
1.3 队列继承链
无论是主队列,全局队列还是自定义队列,都是用dispatch_queue_t
来接的。
点进去查看dispatch_queue_t的定义。
点进去查看DISPATCH_DECL
的定义,发现是OS_OBJECT_DECL_SUBCLASS
的封装。
接下来在源码中搜索OS_OBJECT_DECL_SUBCLASS
,发现其定义。
接下来搜索OS_OBJECT_DECL_IMPL
,发现是类型的拼接。
然后搜索OS_OBJECT_DECL_PROTOCOL
,看到其定义。
往下搜索OS_OBJECT_CLASS
,发现是OS名字的拼接。
那么也就是说DISPATCH_DECL(dispatch_queue);
进来之后,变成了OS_dispatch_queue
,
然后在OS_OBJECT_DECL_PROTOCOL
中根据OS_OBJECT_DECL_IMPL中传来的参数,
@protocol OS_OBJECT_CLASS(name) __VA_ARGS__
变成:@protocol OS_OBJECT_CLASS(OS_dispatch_queue) dispatch_object
。
typedef adhere<OS_OBJECT_CLASS(name)>
变成:typedef NSObject<OS_dispatch_queue>
。
OS_OBJC_INDEPENDENT_CLASS name##_t
变成:OS_OBJC_INDEPENDENT_CLASS OS_dispatch_queue_t
搜索DISPATCH_DECL
的定义,发现其还有这个定义。
typedef struct dispatch_queue_s : public dispatch_object_s {} *dispatch_queue_t
这也就是说,dispatch_queue_t是dispatch_queue_s类型,而dispatch_queue_s继承自dispatch_object_s。
看到这里面有个union,dispatch_object_t
可以代表里面所有的类型,那么也就是说dispatch_object_t
才是真正的根类型
。
这里再搜索dispatch_queue_s
结构体,看到这里有个DISPATCH_QUEUE_CLASS_HEADER
,是继承来的。
接下来搜索DISPATCH_QUEUE_CLASS_HEADER,发现也有一个继承。
接下来搜索DISPATCH_OBJECT_HEADER
,看到其来自于_os_object_s
。
接下来搜索OS_OBJECT_STRUCT_HEADER
。
最后搜索_OS_OBJECT_HEADER
。
那么最后得到GCD的继承链:
dispatch_queue_t
-> dispatch_queue_s
->dispatch_object_s
-> _os_object_s
-> dispatch_object_t
,其最终根类为dispatch_object_t
。
2 GCD的任务执行堆栈
无论是异步还是同步,都有block,那么block是在哪里执行的呢?
2.1 同步队列执行堆栈
正常的调用一个同步这样的。
从函数的调用可以知道,dispatch_sync第一个参数是dispatch_queue_t类型,那么在源码中就可以搜索dispatch_sync(dispatch_queue_t
来进行快速搜索
。
这里的work就是传进来的block,所以需要去看一下work。
搜索一下dispatch_Block_invoke
,看到这里进行了封装
,接下来看这个包装函数在哪里调用执行。
接下来搜索_dispatch_sync_f
。
然后在搜索_dispatch_sync_f_inline
,这里不知道走的哪个函数,就在函数执行的地方下断点。
下一个_dispatch_sync_f_slow
断点后运行。
发现进来了,那么就代表着调用了_dispatch_sync_f_slow
这个函数。
接下来看_dispatch_sync_f_slow的实现,看到这里有对func进行赋值操作。 还有就是_dispatch_sync_function_invoke
和 _dispatch_sync_invoke_and_complete_recurse
。
为_dispatch_sync_function_invoke
和 _dispatch_sync_invoke_and_complete_recurse
打下断点,看看哪个执行。打下断点后运行,发现_dispatch_sync_function_invoke
有被调用,_dispatch_sync_invoke_and_complete_recurse没有。
接下来搜索_dispatch_sync_function_invoke
。
在搜索_dispatch_sync_function_invoke_inline
。
接下来搜索_dispatch_client_callout(void
,发现这里有ctxt的调用执行。
这里也是一样的。
那么就说明,在_dispatch_client_callout就会对block进行调用
。
也可以在block下断点然后bt来证明。
2.1 异步队列执行堆栈
同样的,在底层搜索dispatch_async(dispatch_queue_t
接下来搜索_dispatch_continuation_init(dispatch_continuation_t
,之前的探索中知道 _dispatch_Block_invoke是将work进行封装。
接下来搜索_dispatch_continuation_init_f(dispatch_continuation_t
,看到这里进行了赋值,并且_dispatch_continuation_priority_set
进行了优先级的设定。
那么也就是说,这个分支做了任务的封装以及优先级的封装
。为什么要做封装呢?因为这里是异步函数,说明会异步调用,可能产生无序的情况,那么优先级就是参考的依据。而异步函数代表着任务回调也是异步的,并且是根据cpu的调度情况进行异步。这里对任务进行封装,那么只要cpu说可以执行了,就可以将其拿出来进行调用。
接下来搜索_dispatch_continuation_async(dispatch
。
然后搜索dx_push
。
这里z是qos,我们只关注z,所以搜索dq_push
。
接下来搜索dq_push,发现其在全局队列
时候的赋值。
接下来搜索_dispatch_root_queue_push
,发现最后都会调用_dispatch_root_queue_push_inline
。
搜素一下_dispatch_root_queue_push_inline
。
然后搜索_dispatch_root_queue_poke
。
再搜索_dispatch_root_queue_poke_slow
。
接下来看_dispatch_root_queues_init
。
这里调用了_dispatch_root_queues_init_once
,看到这里有个_dispatch_worker_thread2
。
回到程序里面bt
进行逆向推导,发现这里有调用_dispatch_worker_thread2
。
然后是_dispatch_root_queue_drain
,看到这里的_dispatch_continuation_pop_inline
。
搜索_dispatch_continuation_pop_inline
。
接下来走到_dispatch_continuation_invoke_inline
。
然后调用了_dispatch_client_callout
。
而在_dispatch_client_callout就调用了block
。
3. 面试题
这里的函数会输出什么呢?这里的答案是A:1230789
,因为这里是串行队列,那么任务就会按顺序执行,所以打印出来的是1230789。
这里的运行结果会是什么呢?答案是大于等于5
。这里的while循环,确保了num至少为5,而因为是异步执行self.num++,那么可能在self.num >= 5 的时候,还有线程在运行self.num++,那么如果其在NSLOG之前完成了,那么self.num就大于等于5了。也有一种可能是例如在self.num 等于3时,恰好有多个线程完成了self.num++,那么self.num就会大于等于5,退出循环了。
那么这里的运行结果是多少呢?是不是>= 10000呢?答案是小于等于10000
,因为这里的循环判定条件是i,当 i=10000 退出循环时,下面有的线程可能还没执行完,那么就会小于等于10000了。同时这里线程也不安全,当上一个线程还未进行++操作,值为100时,那么下一个线程读的时候,也是100了,而这个时候上一个线程进行++操作,下一个也进行了++操作,结果都是101。而不是102。