一. 问题背景
很多人都知道GCD的全局队列最多在线程池里面创建64条线程,但是对于:
- 为什么
GCD的全局队列最多只能创建64条线程 - 自定义的多个串行队列、自定义的并发队列,最多可以创建多少条线程, 为什么?
- 自定义的串行队列、自定义的并发队列与全局队列、线程池有怎样的联系。
对于这些问题,可能依然存在一些疑问。
这里就站在之前大佬整理材料的基础上,加上自己的验证结果,做一个分析和总结。
二. 问题探究
-
全局并发队列
首先通过10000次for循环调用全局并发队列的子线程来执行打印任务,并进入休眠。
这里线程数量的打印调用task_threads来获取当前进程任务线程数量,并结合Xcode显示的线程进行分析。
如下是全局队列相关的调试代码:
打印结果:
从日志打印我们可以看到打印出来最大的线程数量是66条线程。
从Xcode的线程分布也可以看出属于GCD的com.apple.root.default-qos (concurrent)队列的线程数量也是总共64条。
然后我们将sleep(10000)休眠动作去掉,将for循环调到10万次,再次测试:
打印结果:
我们可以看到线程数量已经达到68、69。
我们在观察下Xcode线程分布:
我们可以看到属于GCD的com.apple.root.default-qos (concurrent)队列的线程数量依然是总共64条。
多出来的是属于com.apple.CoreAnalytics::Client XPC Reply串行队列的线程。
而此时我们在观察下NSThread的number数量,已经最多可以达到109了。
这也就说明NSThread的number编号,不能用来代表线程池里面的线程数量,因为编号(number)是不能设置的,一般由系统决定,但主线程默认就是1,只要不为1的就是子线程, 而线程池里面的子线程,会根据任务数量、线程状态等因素,进行销毁和新建,新建的线程的number会在之前最后一条新建线程的number后面加1.
因此通过以上实践,我们可以确定GCD线程池里全局队列最大能分配的线程数量是64条。
为什么是 64 而不是别的数字呢?
这是因为GCD创建线程的方式是通过调用_pthread_workqueue_addthreads来实现的,在内核中使用该方法添加的线程数量是有限制的。
stackoverflow.com/questions/7…
限制的具体代码如下所示:
#define MAX_PTHREAD_SIZE 64*1024
该守则的含义如下:
根据Apple-Docs-线程编程指南-线程创建成本:1个线程分配1k内核数据结构,总大小限制在64k,由此可以推断出结果是64个线程。
-
并发队列
首先跟全局并发队列测试方式一致,通过1万次for循环调用自定义的并发队列的子线程来执行打印任务,并进入休眠。
这里线程数量的打印调用task_threads来获取当前进程任务线程数量,并结合Xcode显示的线程进行分析。
打印结果:
从日志打印我们可以看到打印出来最大的线程数量是66条线程。
从Xcode的线程分布也可以看出属于GCD的fjf_test_con_current_queue(concurrent)队列的线程数量也是总共64条。
然后我们将sleep(10000)休眠动作去掉,将for循环调到10万次,再次测试:
我们可以看到线程数量可以达到68
我们可以看到属于GCD的fjf_test_con_current_queue队列的线程数量只有63条,不超过64条。
这里我们可以看到Thread 36、Thread 57、Thread 71这几条线程不像com.apple.uikit.eventfetch-thread、com.apple.main-thread (serial)等有明确归属,也就是这几条线程,在线程池里面处于空闲状态。
从这里我们也可以看出,自定义的并发队列,能创建的线程数量最大也是64条。
这里我们会有疑问,为什么自定义的并发队列和全局并发队列,创建的线程数量都是64条。
是自定义的并发队列和全局并发队列底层都受GCD线程池控制,无论怎样最多只能创建64条线程。
还是自定义的并发队列和全局并发队列,独立开,各自都最多可以创建64条线程。
我们依旧通过1万次for循环调用自定义的并发队列和全局并发队列,来执行打印任务,并进入休眠。
打印结果:
从日志打印我们可以看到即使自定义的并发队列和全局并发队列一起调用,可以创建最大的线程数量是66条线程。
从Xcode的线程分布也可以看出属于GCD的fjf_test_con_current_queue(concurrent)队列和com.apple.root.default-qos (concurrent)的线程数量也是总共64条。
然后我们将sleep(10000)休眠动作去掉,将for循环调到10万次,再次测试:
我们可以看到线程数量可以达到68
从Xcode的线程分布也可以看出属于GCD的fjf_test_con_current_queue(concurrent)队列和com.apple.root.default-qos (concurrent)的线程数量也是总共64条。
从这里无论是自定义的并发队列还是不同优先级的全局并发队列,创建线程都受GCD线程池控制,所有并发队列一起总的最多只能创建64条线程。
原因在于:
我们自定义的并发队列customConcurrentQueue,创建的时候都会设置当前队列的目标队列do_targetq,向自定义并发队列提交的block(即任务),都会被放到它的目标队列中执行。而我们调用GCD的dispatch_queue_create方法创建的自定义并发队列的目标队列只能是LOW、DEFAULT、HIGH 和 BACKGROUND不支持overcommint的4个全局队列。
这四个全局队列底层是一个线程池,创建线程的方式都似乎通过_pthread_workqueue_addthreads来实现,因此也就受内核中线程数量的限制,最多只能创建64条。这点上面全局并发队列有提到。
具体详见: [深入理解GCD]
-
串行队列
因为串行队列一次只能有一条线程去执行任务,因此串行队列这里验证的是循环创建1万个串行队列,去执行任务,到底会创建多少条线程。
打印结果:
我们可以看到即使我们循环创建了1万个串行队列去执行任务,最多也只能创建514条线程。
从Xcode的线程分布也可以看出属于GCD的fjf_test_con_current_queue_XXX (serial)串行队列的线程数量最多512条。
为什么是 512 条线程呢?
通过dispatch_queue_create创建的自定义的串行队列,创建的时候都会设置当前队列的目标队列do_targetq,默认是开启overcommit,也就是说普通串行队列的目标队列就是一个支持 overcommit 的全局队列,而带有 overcommit 的队列表示每当有任务提交时,系统都会新开一个线程处理,这样就不会造成某个线程过载(overcommit)。
而GCD线程池里面最多开辟512条线程,应该是系统给每个进程的限制。
因为我们同时开启全局并发队列和1万条串行队列去执行任务,然后休眠,最多也只能开辟512条。
也就是整个进程最多可以开辟516条线程 = 512(max) +主线程+ js线程+ web线程+ uikit事件线程
三. 问题总结
-
并发队列
A. 结论:
GCD的全局并发队列和自定义并发队列,所有并发队列一起最多只能创建64条线程,因此单个自定义并发队列和全局并发队列最多也只能创建64条线程。
B. 原因:
自定义并发队列customConcurrentQueue,创建的时候都会设置当前队列的目标队列do_targetq,向自定义并发队列提交的block(即任务),都会被放到它的目标队列中执行。而我们调用GCD的dispatch_queue_create方法创建的自定义并发队列的目标队列只能是LOW、DEFAULT、HIGH 和 BACKGROUND不支持overcommint的4个全局队列。
这四个全局队列底层是一个线程池,创建线程的方式都似乎通过_pthread_workqueue_addthreads来实现,在内核中使用该方法添加的线程数量是有限制的。
限制的具体代码如下所示:
#define MAX_PTHREAD_SIZE 64*1024
该守则的含义如下:
根据Apple-Docs-线程编程指南-线程创建成本:1个线程分配1k内核数据结构,总大小限制在64k,由此可以推断出结果是64个线程。
-
串行队列
A. 结论
无论创建多少个串行队列,最多只能开辟512条线程。
B. 原因
通过dispatch_queue_create创建的自定义的串行队列,创建的时候都会设置当前队列的目标队列do_targetq,默认是开启overcommit,也就是说普通串行队列的目标队列就是一个支持 overcommit 的全局队列,而带有 overcommit 的队列表示每当有任务提交时,系统都会新开一个线程处理,这样就不会造成某个线程过载(overcommit)。
而GCD线程池里面最多开辟512条线程,应该是系统给每个进程的限制。
也就是整个进程最多可以开辟516条线程 = 512(max) +主线程+ js线程+ web线程+ uikit事件线程