前阵子,在读者交流群中有人提到 Go 默认设置的最大线程数的问题:如果超过一万个 G (挂载于 M 上)阻塞于系统调用,那么程序就会被挂掉。

这是对的,因为 Go 对运行时创建的线程数量有一个限制,默认是 10000 个线程。今天我们就来探讨一下 Go 线程数相关的问题。

闲置线程

相信对 Go 有所了解的人,对下图所示的 GMP 模型不会陌生,每个 P 都会有一个操作系统线程 M 来执行其上的 G。

GMP 模型

我们可以通过设置 GOMAXPROCS 来设定 P 的最大值,这个值代表什么含义呢?

The GOMAXPROCS variable limits the number of operating system threads that
can execute user-level Go code simultaneously. There is no limit to the number of threads
that can be blocked in system calls on behalf of Go code; those do not count against
the GOMAXPROCS limit. This package's GOMAXPROCS function queries and changes
the limit.

通过 GOMAXPROCS 的定义文档,我们可以看到该变量只是限制了可以同时执行用户级 Go 代码的 OS 系统线程数量(通俗地讲:Go 程序最多只能有和 P 相等个数的系统线程同时运行)。但是,在系统调用中被阻塞的线程不在此限制之中。

对于系统调用,可分为同步和异步两种方式。

我们在《Go 网络编程和 TCP 抓包实操》一文中阐述的 Go 网络编程模型,就是一种异步系统调用。它使用网路轮询器进行系统调用,调度器可以防止 G 在进行这些系统调用时阻塞 M。这可以让 M 继续执行其他的 G,而不需要创建新的 M。

但是,如果 G 要进行的是无法异步完成的系统调用时怎么办?当网络轮询器无法使用时,进行系统调用的 G 将会阻塞 M。在 Linux 下基于普通文件(Linux 下的 epoll 只支持 socket,Windows 下的 iocp 可以支持 socket、file)的系统调用就是一个典型的例子。

同步系统调用 1

如上图所示,运行在 M1 上的 G1 想要请求一个同步系统调用。

同步系统调用 2

当发生同步系统调用并阻塞时,调度器将 M1 和仍然挂载在其之上的 G1 与 P 分离,并引入新的 M2 来运行 P 上的其他 G。

同步系统调用 3

当 G1 进行的阻塞式系统调用结束时,G1 重新回到 P 的 LRQ 中去,但 M1 变成了闲置线程,不会被回收,以留备后续复用。

问题来了,如果在某一短时段内,Go 程序存在大量无法短暂结束的同步系统调用,那线程数岂不是会一直涨下去?

最大线程数限制

线程数限制的问题,在官方 issues#4056: "runtime: limit number of operating system threads" 中,有过讨论,并最终将线程限制数值确定为 10000。

这个值存在的主要目的是限制可以创建无限数量线程的 Go 程序:在程序把操作系统干爆之前,干掉程序。

当然,Go 也暴露了 debug.SetMaxThreads() 方法可以让我们修改最大线程数值。

package mainimport ("os/exec""runtime/debug""time"
)func main() {debug.SetMaxThreads(10)for i := 0; i < 20; i++ {go func() {_, err := exec.Command("bash", "-c", "sleep 3").Output()if err != nil {panic(err)}}()}time.Sleep(time.Second * 5)
}

如程序所示,我们将最大线程数设置为 10,然后通过执行 shell 命令 sleep 3 来模拟同步系统调用过程。那么,执行 sleep 操作的 G 和 M 都会阻塞,当程序启动的线程 M 超过 10 个时,会得到以下报错。

runtime: program exceeds 10-thread limit
fatal error: thread exhaustion
***

让闲置线程退出

闲置线程退出的问题,在官方 issues#14592: "runtime: let idle OS threads exit" 中有过讨论。目前,还没有一个完美的解决方案。

但是,在该 issue 里有人提出使用 runtime.LockOSThread() 方法来杀死线程。

简单了解下这个函数的特性:

  • 调用 LockOSThread 函数会把当前 G 绑定在当前的系统线程 M 上,这个 G 总是在这个 M 上执行,并且阻止其它 G 在该 M 执行。

  • 只有当前 G 调用了与之前调用 LockOSThread 相同次数的 UnlockOSThread 函数之后,G 与 M 才会解绑。

  • 如果当前 G 在退出时,没有调用 UnlockOSThread,这个线程会被终止。

那么,我们可以利用第三个特性,在启动 G 时,调用 LockOSThread 来独占一个 M。当 G 退出时,而不调用 UnlockOSThread,那这个 M 将不会被闲置,就被终止了。

下面,我们来看一个例子

package mainimport ("fmt""os/exec""runtime/pprof""time"
)func main() {threadProfile := pprof.Lookup("threadcreate")fmt.Printf(" init threads counts: %d\n", threadProfile.Count())for i := 0; i < 20; i++ {go func() {_, err := exec.Command("bash", "-c", "sleep 3").Output()if err != nil {panic(err)}}()}time.Sleep(time.Second * 5)fmt.Printf(" end threads counts: %d\n", threadProfile.Count())
}

通过 threadProfile.Count() 我们可以实时获取当前线程数目,那么在发生了阻塞式系统调用后,该程序的线程数目是多少呢?

init threads counts: 5end threads counts: 25

根据结果可以看到,G 执行完毕后,闲置线程并没有被释放。

在程序中添加一行代码 runtime.LockOSThread() 代码

go func() {runtime.LockOSThread() // 增加的一行代码_, err := exec.Command("bash", "-c", "sleep 3").Output()if err != nil {panic(err)}}()

此时,程序的执行结果如下

init threads counts: 5end threads counts: 11

可以看到,由于调用了 LockOSThread 函数的 G 没有执行 UnlockOSThread 函数,在 G 执行完毕后,M 也被终止了。

总结

在 GMP 模型中,P 与 M 一对一的挂载形式,通过设定 GOMAXPROCS 变量就能控制并行线程数。

当 M 遇到同步系统调用时,G 和 M 会与 P 剥离,当系统调用完成,G 重新进入可运行状态,而 M 就会被闲置起来。

Go 目前并没有对闲置线程做清除处理,它们被当作复用的资源,以备后续需要。但是,如果在 Go 程序中积累大量空闲线程,这是对资源的一种浪费,同时对操作系统也产生了威胁。因此,Go 设定了 10000 的默认线程数限制。

我们发现了一种利用 LockOSThread 函数的 trik 做法,可以借此做一些限制线程数的方案:例如启动定期排查线程数的 goroutine,当发现程序的线程数超过某阈值后,就回收掉一部分闲置线程。

当然,这个方法也存在隐患。例如在 issues#14592 有人提到:当子进程由一个带有 PdeathSignal: SIGKILL 的 A 线程创建,A 变为空闲时,如果 A 退出,那么子进程将会收到 KILL 信号,从而引起其他问题。

当然,绝大多数情况下,我们的 Go 程序并不会遇到空闲线程数过多的问题。如果真的存在线程数暴涨的问题,那么你应该思考代码逻辑是否合理(为什么你能允许短时间内如此多的系统同步调用),是否可以做一些例如限流之类的处理。而不是想着通过 SetMaxThreads 方法来处理。

参考

Scheduling In Go:https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part2.html

issues#4056:https://github.com/golang/go/issues/4056

issues#14592:https://github.com/golang/go/issues/14592

往期推荐

  • 几个秒杀 Go 官方库的第三方开源库

  • Go 中如何强制关闭 TCP 连接

  • 我终于识破了这个 Go 编译器把戏

  • 被遗弃在角落里的 sync.Cond

机器铃砍菜刀

欢迎添加小菜刀微信

加入Golang分享群学习交流!

感谢你的点赞在看哦~

如何有效控制 Go 线程数?相关推荐

  1. 并发工具类(三)控制并发线程数的Semaphore

    http://ifeve.com/concurrency-semaphore/ 简介 Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源.很 ...

  2. 并发编程-17AQS同步组件之 Semaphore 控制并发线程数的信号量

    文章目录 J.U.C脑图 Semaphore 概述 Semaphore 构造函数及方法 构造函数 常用和需要注意的方法 Semaphore 示例 acquire() semaphore.acquire ...

  3. JAVA线程并发数量控制_Java并发工具类(三):控制并发线程数的Semaphore

    作用 Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源. 简介 Semaphore也是一个线程同步的辅助类,可以维护当前访问自身的线程个数 ...

  4. python限制最大线程数(不一样的方法)

    实际工作中会遇到有成千上万个任务想使用多线程来完成,而计算机无法同时处理这么多线程,通常来说会因为内存消耗完而无法创建新的线程. 在诸多大佬的博客中找到如下解决方案: import threading ...

  5. Shell多线程操作及线程数控制实例

    来源:http://www.jb51.net/article/51720.htm 这篇文章主要介绍了Shell多线程操作及线程数控制实例,文中从单线程实现一个需求开始,不断加入代码实现多线程以及线程数 ...

  6. Python继承类的方式实现多线程及控制线程数

    继承threading.Thread,并重写run方法实现多线程,这里用到logging日志模块是为了输出好看一些,直接print的话会几行叠在一起,不好看: 1 2 3 4 5 6 7 8 9 10 ...

  7. Java发令枪ConcurrentExecutor之控制线程数,并发数

    前言 前段时候学习单例模式的时候,有用到多线程并发数去测试单例模式的线程安全.但是当时时间比较紧没有进行记录,今天特地记录一下. 1.先看代码 import java.util.concurrent. ...

  8. Python SSH爆破以及Python3线程池控制线程数

    源自一个朋友的要求,他的要求是只爆破一个ip,结果出来后就停止,如果是爆破多个,完全没必要停止,等他跑完就好 #!usr/bin/env python #!coding=utf-8__author__ ...

  9. 面试官问:高并发下,你都怎么选择最优的线程数?

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 来源 | https://urlify.cn/YFbMNf 为了加 ...

最新文章

  1. 三门问题(蒙提霍尔悖论)的Python代码实现
  2. 简易在线实验室管理系统
  3. Hibernate:hbm2ddl.auto =在生产中更新?
  4. Android 不同阶段 Logo 显示
  5. Linux下添加DB2用户
  6. 第一章:1.1 信号表征
  7. CSS display: table-cell 用于水平垂直居中
  8. webpack把源代码里面的console和debugger全部去掉_在webpack中,module、chunk和bundle到底是什么样的存在?...
  9. docker自定义端口挂载目录
  10. This blog has been cancelled for a long time
  11. 男子刷机多次拒绝来电 维修师傅的做法被网友怒赞
  12. 95-36-210-ChannelHandler-系统Channel-TimeoutHandler
  13. Google叫停出售刷脸监控技术,只因目前无法避免被滥用
  14. 什么叫ServletContext对象?
  15. Python MySQL 数据库
  16. VB 2010中Excel文件处理的一个奇怪问题
  17. 在delphi中调用chm帮助文件_delphi教程
  18. 手机浏览器调用摄像头扫码
  19. 不能错过!9个让你随时保持精力充沛的习惯
  20. coursera—Mini-project #1 - Rock-paper-scissor-lizard-Spock

热门文章

  1. ERROR: Could not read unit serial number!
  2. VS2017 installed in a different location
  3. prim 算法加模板
  4. 马云致投资者公开信:大数据云计算是阿里未来十年核心战略之一
  5. EBS模块介绍和概念解释
  6. TClientDataSet[22]: 数组字段与 ObjectView
  7. CodeForces - 1523D Love-Hate(随机数+状压dp)
  8. CodeForces - 1207F Remainder Problem(分块)
  9. CodeForces - 1370D Odd-Even Subsequence(二分+思维)
  10. HDU - 4511 小明系列故事——女友的考验(AC自动机+dp)