多线程一直是我相当感兴趣的技术知识之一, 个人尤其喜爱 GCD 这个轻量级的多线程解决方案, 为了了解其实现, 不厌其烦的翻阅 libdispatch 的源码甚至因为太喜欢了, 本来想要写这相应的源码解析系列文章, 但害怕写的不好, 于是除了开篇的类型介绍, 也是草草了事, 没了下文
恰好这几天好友出了几道有关 GCD 的题目, 运行结果出于意料, 仔细摸索后, 发现苹果基于 libdispatch 做了一些有趣的修改工作, 于是想将这两道题目分享出来由于朋友提供的运行代码为 Swift 书写, 在此我转换成等效的 OC 代码进行讲述你如果了解了下面两个概念, 会让后续的阅读更加容易:
同步与异步的概念
队列与线程的区别
被误解的概念
对于主线程和主队列, 我们可能会有这么一个理解
主线程只会执行主队列的任务同样, 主队列只会在主线程上被执行
主线程只会执行主队列的任务
首先是主线程只会执行主队列的任务在 iOS 中, 只有主线程才拥有权限向渲染服务提交打包的图层树信息, 完成图形的显示工作而我们在 work queue 中提交的 UI 更新总是无效的, 甚至导致崩溃发生而由于主队列只有一条, 其他的队列全部都是 work queue, 因此可以得出
主线程只会执行主队列的任务
这一结论但是, 有下面这么一段代码:
- dispatch_queue_t mainQueue = dispatch_get_main_queue();
- dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
- dispatch_queue_set_specific(mainQueue, "key", "main", NULL);
- dispatch_sync(globalQueue, ^{
- BOOL res1 = [NSThread isMainThread];
- BOOL res2 = dispatch_get_specific("key") != NULL;
- NSLog(@"is main thread: %zd --- is main queue: %zd", res1, res2);
- });
根据正常逻辑的理解来说, 这里的两个判断结果应该都是 NO, 但运行后, 第一个判断为 YES, 后者为 NO, 输出说明了主线程此时执行了 work queue 的任务
dispatch_sync
上面的代码在换成 async 之后就会得到预期的判断结果, 但在同步执行的情况下就会导致这个问题在查找原因之前, 借用 bestswifter 文章中的代码一用, 首先 sync 的调用栈以及大致源码如下:
- dispatch_sync
- dispatch_sync_f
- _dispatch_sync_f2
- _dispatch_sync_f_slow
- static void _dispatch_sync_f_slow(dispatch_queue_t dq, void *ctxt, dispatch_function_t func) {
- _dispatch_thread_semaphore_t sema = _dispatch_get_thread_semaphore();
- struct dispatch_sync_slow_s {
- DISPATCH_CONTINUATION_HEADER(sync_slow);
- } dss = {
- .do_vtable = (void*)DISPATCH_OBJ_SYNC_SLOW_BIT,
- .dc_ctxt = (void*)sema,
- };
- _dispatch_queue_push(dq, (void *)&dss);
- _dispatch_thread_semaphore_wait(sema);
- _dispatch_put_thread_semaphore(sema);
- // ...
- }
可以看到对于 libdispatch 对于同步任务的处理是采用 sema 信号量的方式堵塞调用线程直到任务被处理完成, 这也是为什么 sync 嵌套使用是一个死锁问题根据源码可以得到执行的流程图:
但实际运行后, block 是执行在主线程上的, 代码真正流程是这样的:
因此可以做一个猜想:
由于 sync 函数本身会堵塞当前执行线程直到任务执行为了减少线程切换的开销, 以及避免线程被堵塞的资源浪费, 于是对 sync 函数进行了改进: 在大多数情况下, 直接在当前线程执行同步任务
既然有了猜想, 就需要验证之所以说是大多数情况, 是因为目前
主队列只在主线程上被执行
还是有效的, 因此我们排除
global -sync-> main
这种条件因此为了验证效果, 需要创建一个串行线程:
- dispatch_queue_t serialQueue = dispatch_queue_create("serial.queue", DISPATCH_QUEUE_SERIAL);
- dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
- dispatch_sync(globalQueue, ^{
- BOOL res1 = [NSThread isMainThread];
- BOOL res2 = dispatch_get_specific("key") != NULL;
- NSLog(@"is main thread: %zd --- is main queue: %zd", res1, res2);
- });
- dispatch_async(globalQueue, ^{
- NSThread *globalThread = [NSThread currentThread];
- dispatch_sync(serialQueue, ^{
- BOOL res = [NSThread currentThread] == globalThread;
- NSLog(@"is same thread: %zd", res);
- });
- });
运行后, 两次判断的结果都是 YES, 结果足以验证猜想, 可以确定苹果为了提高性能, 已经对 sync 做了修改另外
global -sync-> main
测试结果发现 sync 的调用过程不会被优化
主队列只会在主线程上执行
上面说过, 只有主线程才有权限提交渲染任务同样的, 出于下面两个设定, 这个理解应当是成立的:
主队列总是可以调用 UIKit 的接口 api
同时只有一条线程能够执行串行队列的任务
同样的, 朋友给出了另一份代码, 但由于代码中存在一个 Swift 的关键函数, 因此直接展示原代码:
- let key = DispatchSpecificKey<String>()
- DispatchQueue.main.setSpecific(key: key, value: "main")
- func log() {
- debugPrint("main thread: \(Thread.isMainThread)")
- let value = DispatchQueue.getSpecific(key: key)
- debugPrint("main queue: \(value != nil)")
- }
- DispatchQueue.global().async {
- DispatchQueue.main.async(execute: log)
- }
- dispatchMain()
运行之后, 输出结果分别为 NO 和 YES, 也就是说此时主队列的任务并没有在主线程上执行要弄清楚这个问题的原因显然难度要比上一个问题难度大得多, 因为如果子线程可以执行主队列的任务, 那么此时是无法提交打包图层信息到渲染服务的
同样的, 我们可以先猜测原因不同于正常的项目启动代码, 这个 Swift 文件的运行更像是脚本运行, 因为缺少了一段启动代码:
- @autoreleasepool
- {
- return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
- }
为了找到答案, 首先需要对问题
主线程只会执行主队列的任务
的代码进行改造一下另外由于第二个问题涉及到
执行任务所在的线程
,mach_thread_self 函数会返回当前线程的 id, 可以用来判断两个线程是否相同:
- thread_t threadId = mach_thread_self();
- dispatch_queue_t mainQueue = dispatch_get_main_queue();
- dispatch_queue_t serialQueue = dispatch_queue_create("serial.queue", DISPATCH_QUEUE_SERIAL);
- dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
- dispatch_async(globalQueue, ^{
- dispatch_async(mainQueue, ^{
- NSLog(@"%zd --- %zd", threadId == mach_thread_self(), [NSThread isMainThread]);
- });
- });
- @autoreleasepool
- {
- return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
- }
这段代码的运行结果都是 YES, 说明在 UIApplicationMain 函数前后主队列任务执行的线程 id 是相同的, 因此可以得出两个条件:
主队列的任务总是在同一个线程上执行
在 UIApplicationMain 函数调用后, isMainThread 返回了正确结果
结合这两个条件, 可以做出猜想: 在 UIApplicationMain 中存在某个操作使得原本执行主队列任务的线程变成了主线程, 其猜想图如下:
由于 UIApplicationMain 是个私有 api, 我们没有其实现代码, 但是我们都知道在这个函数调用之后, 主线程的 runloop 会被启动, 那么这个线程的变动是不是跟 runloop 的启动有关呢? 为了验证这个判断, 在手动启动 runloop 定时的去检测线程:
- func log() {
- debugPrint("is main thread: \(Thread.isMainThread)")
- DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: log)
- }
- DispatchQueue.global().async {
- DispatchQueue.main.async(execute: log)
- }
- RunLoop.current.run()
在 runloop 启动后, 所有的检测结果都是 YES:
- // console log
- "is main thread: true"
- "is main thread: true"
- "is main thread: true"
- "is main thread: true"
- "is main thread: true"
- "is main thread: true"
- "is main thread: true"
- "is main thread: true"
- "is main thread: true"
- "is main thread: true"
- "is main thread: true"
- "is main thread: true"
- "is main thread: true"
- "is main thread: true"
代码的运行结果验证了这个猜想, 但结论就变成了:
thread -> runloop -> main thread
这样的结论, 随便启动一个 work queue 的 runloop 就能轻易的推翻这个结论, 那么是否可能只有第一次启动 runloop 的线程才有可能变成主线程? 为了验证这个猜想, 继续改造代码:
- let serialQueue = DispatchQueue(label: "serial.queue")
- func logSerial() {
- debugPrint("is main thread: \(Thread.isMainThread)")
- serialQueue.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: logSerial)
- }
- serialQueue.async {
- RunLoop.current.run()
- }
- DispatchQueue.global().async {
- serialQueue.async(execute: logSerial)
- }
- dispatchMain()
在保证了子线程的 runloop 是第一个被启动的情况下, 所有运行的输出结果都是 false, 也就是说因为 runloop 修改了线程的 priority 的猜想是不成立的, 那么基于 UIApplicationMain 测试代码的两个条件无法解释
主队列为什么没有运行在主线程上
主队列不总是在同一个线程上执行
经过来回推敲, 我发现
主队列总是在同一个线程上执行
这个条件限制了进一步扩大猜想的可能性, 为了验证这个条件, 通过定时输出主队列任务所在的 threadId 来检测这个条件是否成立:
- let threadId = mach_thread_self()
- let serialQueue = DispatchQueue(label: "serial.queue")
- debugPrint("current thread id is: \(threadId)")
- func logMain() {
- debugPrint("=====main queue======> thread id is: \(mach_thread_self())")
- DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: logMain)
- }
- func logSerial() {
- debugPrint("serial queue thread id is: \(mach_thread_self())")
- serialQueue.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: logSerial)
- }
- DispatchQueue.global().async {
- serialQueue.async(execute: logSerial)
- DispatchQueue.main.async(execute: logMain)
- }
- dispatchMain()
在测试代码中增加子队列定时做对比, 发现不管是 serial queue 还是 main queue, 都有可能运行在不同的线程上面但是如果去掉了子队列作为对比, main queue 只会执行在一条线程上, 但该线程的 threadId 总是不等同于我们保存下来的数值:
- // console log
- current thread id is: 775
- "serial queue thread id is: 6403"
- "=====main queue======> thread id is: 7171"
- "serial queue thread id is: 6403"
- "=====main queue======> thread id is: 6403"
- "serial queue thread id is: 6403"
- "=====main queue======> thread id is: 6403"
- "serial queue thread id is: 6403"
- "=====main queue======> thread id is: 6403"
- "serial queue thread id is: 6403"
- "=====main queue======> thread id is: 1547"
- "serial queue thread id is: 1547"
- "=====main queue======> thread id is: 6403"
- "serial queue thread id is: 6403"
- "=====main queue======> thread id is: 1547"
- "serial queue thread id is: 1547"
- "=====main queue======> thread id is: 6403"
- "=====main queue======> thread id is: 1547"
- "serial queue thread id is: 6403"
- "serial queue thread id is: 4355"
- "=====main queue======> thread id is: 6403"
- "serial queue thread id is: 6403"
- "=====main queue======> thread id is: 4355"
- "=====main queue======> thread id is: 6403"
- "serial queue thread id is: 4355"
- "=====main queue======> thread id is: 4355"
- "serial queue thread id is: 6403"
- "serial queue thread id is: 1547"
- "=====main queue======> thread id is: 6403"
- "serial queue thread id is: 6403"
- "=====main queue======> thread id is: 1547"
- "=====main queue======> thread id is: 6403"
- "serial queue thread id is: 1547"
- "serial queue thread id is: 6403"
- "=====main queue======> thread id is: 1547"
- "serial queue thread id is: 1547"
发现了这一个新的现象后, 结合之前的信息来看, 可以得出一个新的猜想:
有一个专用启动线程用于启动主线程的 runloop, 启动前主队列会被这个线程执行
要测试这个猜想也很简单, 只要对比 runloop 前后的 threadId 是否一致就可以了:
- let threadId = mach_thread_self()
- debugPrint("current thread id is: \(threadId)")
- func logMain() {
- debugPrint("=====main queue======> thread id is: \(mach_thread_self())")
- DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: logMain)
- }
- DispatchQueue.global().async {
- DispatchQueue.main.async(execute: logMain)
- }
- RunLoop.current.run()
- // console log
- current thread id is: 775
- "=====main queue======> thread id is: 775"
- "=====main queue======> thread id is: 775"
- "=====main queue======> thread id is: 775"
- "=====main queue======> thread id is: 775"
- "=====main queue======> thread id is: 775"
- "=====main queue======> thread id is: 775"
- "=====main queue======> thread id is: 775"
- "=====main queue======> thread id is: 775"
- "=====main queue======> thread id is: 775"
运行结果说明了并不存在什么启动线程, 一旦 runloop 启动后, 主队列就会一直执行在同一个线程上, 而这个线程就是主线程由于 runloop 本身是一个不断循环处理事件的死循环, 这才是它启动后主队列一直运行在一个主线程上的原因最后为了测试启动 runloop 对串行队列的影响, 单独启动子队列和一起启动后, 发现另一个现象:
主队列的 runloop 一旦启动, 就只会被该线程执行任务
子队列的 runloop 无法绑定队列和线程的执行关系
由于在源码中 async 调用对于主队列和子队列的表现不同, 后者会直接启用一个线程来执行子队列的任务, 这就是导致了 runloop 在主队列和子队列上差异化的原因, 也能说明苹果并没有大肆修改 libdispatch 的源码
其他
过了一个漫长的春节假期之后, 感觉急需一个节假日来休息, 可惜这只是奢望由于节后综合征, 在这周重新返工的状态感觉一般, 也偶尔会提不起神来, 希望自己尽快恢复过来另外随着不断的积累, 一些自以为熟悉的奇怪问题又总能带来新的认知和收获, 我想这就是学习最大的快乐了
关于使用代码
由于 Swift 语法上和 OC 始终存在差异, 第二段代码并不能很好的还原, 如果对此感兴趣的朋友可以关注下方仓鼠大佬的博客链接, 大佬放话后续会放出源码另外如果不想阅读 libdispatch 源码又想对这部分的逻辑有所了解的朋友可以看下面的链接文章
来源: https://juejin.im/post/5a9a4b96f265da238b7d764b