-
Notifications
You must be signed in to change notification settings - Fork 190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
避免使用 GCD Global 队列创建 Runloop 常驻线程 #9
Comments
有个朋友担心“NSThread创建太多,应该也会引起线程调度频繁,资源竞争大吧,global 队列创建太多常驻线程,也可能使得系统在global 队列运行的任务迟迟得不到执行吧”,这个担心是有道理的,这个需要在最外层限制并发数量。无限制也会导致线程泄露。 |
分析不错,结论是不是应该改成:避面长期占用GCD全局队列进行耗时操作 |
是的,数据库和网络请求我上面都给例子了。 |
使用NSThread或者GCD serial queue做一些异步请求任务的timeout判断,一般选择哪一个比较好呢? |
iTeaTime(技术清谈)@SAGESSE-深圳-某不知名小作坊: 问题来了,如果一个gcd任务在runloop,还能加入任务吗" iTeaTime(技术清谈)@ChenYilong : 看gcd队列类型吧,串行队列还是并行队列吧。 iTeaTime(技术清谈)@林小达-鹅厂-iOS :串行类型的话处理不了,除非强制用CFRunloopPerformBlock处理 iTeaTime(技术清谈)@独醉年华-云图iOS -北京 : Q: 弱弱的问下,runloop不是会休眠的么,有事干事没事睡眠 iTeaTime(技术清谈)@ChenYilong : |
Q: 龙哥你这个例子,#9 我怎么一直不崩。切了很多次,就卡死,一直没崩,终于崩了。。。插着电脑调试一直不崩,不插就容易 崩。 A:插着电脑,用的是电脑的CPU,不能比。 |
这个bad case还有一个细节没有被注意到: 可以引申总结为: 什么情况下, 异步线程会导致watchdog? @iteatimeteam (技术清谈) [2021-10-11 21:17:41] 这里的case看似是异步的场景 , 实际crash的更本原因还是主线程, 因为watchdog必定是主线程引起的, 这里的代码跟主线程有什么关系? 主线程只做了一件事情, 那就是派发队列, 反证法也能证明crash在派发队列这里. |
避免使用 GCD Global队列创建Runloop常驻线程
本文对应 Demo 以及 Markdown 文件在仓库中,文中的错误可以提 PR 到这个文件,我会及时更改。
目录
GCD Global队列创建线程进行耗时操作的风险
先思考下如下几个问题:
答案大致是这样的:dispatch_async 函数分发到全局队列不一定会新建线程执行任务,全局队列底层有一个的线程池,如果线程池满了,那么后续的任务会被 block 住,等待前面的任务执行完成,才会继续执行。如果线程池中的线程长时间不结束,后续堆积的任务会越来越多,此时就会存在 APP crash的风险。
比如:
以上逻辑用真机测试会有卡死的几率,并非每次都会发生,但多尝试几次就会复现,伴随前后台切换,crash几率增大。
下面做一下分析:
参看 GCD 源码我们可以看到全局队列的相关源码如下:
对于执行的任务来说,所执行的线程具体是哪个线程,则是通过 GCD 的线程池(Thread Pool)来进行调度,正如Concurrent Programming: APIs and Challenges文章里给的示意图所示:
上面贴的源码,我们关注如下的部分:
其中有一个用来记录线程池大小的字段
dgq_thread_pool_size
。这个字段标记着GCD线程池的大小。摘录上面源码的一部分:从源码中我们可以对应到官方文档 :Getting the Global Concurrent Dispatch Queues里的说法:
也就是说:
全局队列的底层是一个线程池,向全局队列中提交的 block,都会被放到这个线程池中执行,如果线程池已满,后续再提交 block 就不会再重新创建线程。这就是为什么 Demo 会造成卡顿甚至冻屏的原因。
避免使用 GCD Global 队列创建 Runloop 常驻线程
在做网路请求时我们常常创建一个 Runloop 常驻线程用来接收、响应后续的服务端回执,比如NSURLConnection、AFNetworking等等,我们可以称这种线程为 Runloop 常驻线程。
正如上文所述,用 GCD Global 队列创建线程进行耗时操作是存在风险的。那么我们可以试想下,如果这个耗时操作变成了 runloop 常驻线程,会是什么结果?下面做一下分析:
先介绍下 Runloop 常驻线程的原理,在开发中一般有两种用法:
单一 Runloop 常驻线程
先说第一种用法:
以 AFNetworking 为例,AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:
多个 Runloop 常驻线程
第二种用法,我写了一个小 Demo 来模拟这种场景,
我们模拟了一个场景:假设所有的网络请求全部超时,或者服务端根本不响应,然后网络库超时检测机制的做法:
如果
以上逻辑用真机测试会有卡死的几率,并非每次都会发生,但多尝试几次就会复现,伴随前后台切换,crash几率增大。
其中我们采用了 GCD 全局队列的方式来创建常驻线程,因为在创建时可能已经出现了全局队列的线程池满了的情况,毕竟
libdispatch
的最大线程数量非常高(具体数值请参考 StackOverflow - Number of threads created by GCD? ),所以 GCD 派发的任务,无法执行,而且我们把超时检测的逻辑放进了这个任务中,所以导致的情况就是,一旦有很多任务的超时检测功能失效了,此时就只能依赖于服务端响应来结束该任务(服务端响应能结束该任务的逻辑在 Demo 中未给出),但是如果再加之服务端不响应,那么任务就永远不会结束。后续的网络请求也会就此 block 住,造成 crash。如果我们把 GCD 全局队列换成 NSThread 的方式,那么就可以保证每次都会创建新的线程。
注意:文章中只演示的是超时 cancel runloop 的操作,实际项目中一定有其他主动 cancel runloop 的操作,就比如网络请求成功或失败后需要进行cancel操作。代码中没有展示网络请求成功或失败后的 cancel 操作。
Demo 的这种模拟可能比较极端,但是如果你维护的是一个像 AFNetworking 这样的一个网络库,你会放心把创建常驻线程这样的操作交给 GCD 全局队列吗?因为整个 APP 是在共享一个全局队列的线程池,那么如果 APP 把线程池沾满了,甚至线程池长时间占满且不结束,那么 AFNetworking 就自然不能再执行任务了,所以我们看到,即使是只会创建一条常驻线程, AFNetworking 依然采用了 NSThread 的方式而非 GCD 全局队列这种方式。
注释:以下方法存在于老版本AFN 2.x 中。
正如你所看到的,没有任何一个库会用 GCD 全局队列来创建常驻线程,而你也应该
有个朋友担心“NSThread创建太多,应该也会引起线程调度频繁,资源竞争大吧,global 队列创建太多常驻线程,也可能使得系统在global 队列运行的任务迟迟得不到执行吧”,这个担心是有道理的,这个需要在最外层限制并发数量。无限制也会导致线程泄露。
所以也可以说:
Posted by Posted by 微博@iOS程序犭袁 & 公众号@iTeaTime技术清谈
原创文章,版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0
The text was updated successfully, but these errors were encountered: