自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

如何有效控制 Go 線程數(shù)?

開(kāi)發(fā) 后端
相信對(duì) Go 有所了解的人,對(duì)下圖所示的 GMP 模型不會(huì)陌生,每個(gè) P 都會(huì)有一個(gè)操作系統(tǒng)線程 M 來(lái)執(zhí)行其上的 G。

[[431064]]

前陣子,在讀者交流群中有人提到 Go 默認(rèn)設(shè)置的最大線程數(shù)的問(wèn)題:如果超過(guò)一萬(wàn)個(gè) G (掛載于 M 上)阻塞于系統(tǒng)調(diào)用,那么程序就會(huì)被掛掉。

這是對(duì)的,因?yàn)?Go 對(duì)運(yùn)行時(shí)創(chuàng)建的線程數(shù)量有一個(gè)限制,默認(rèn)是 10000 個(gè)線程。今天我們就來(lái)探討一下 Go 線程數(shù)相關(guān)的問(wèn)題。

閑置線程

相信對(duì) Go 有所了解的人,對(duì)下圖所示的 GMP 模型不會(huì)陌生,每個(gè) P 都會(huì)有一個(gè)操作系統(tǒng)線程 M 來(lái)執(zhí)行其上的 G。

GMP 模型

我們可以通過(guò)設(shè)置 GOMAXPROCS 來(lái)設(shè)定 P 的最大值,這個(gè)值代表什么含義呢?

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.

通過(guò) GOMAXPROCS 的定義文檔,我們可以看到該變量只是限制了可以同時(shí)執(zhí)行用戶級(jí) Go 代碼的 OS 系統(tǒng)線程數(shù)量(通俗地講:Go 程序最多只能有和 P 相等個(gè)數(shù)的系統(tǒng)線程同時(shí)運(yùn)行)。但是,在系統(tǒng)調(diào)用中被阻塞的線程不在此限制之中。

對(duì)于系統(tǒng)調(diào)用,可分為同步和異步兩種方式。

我們?cè)凇禛o 網(wǎng)絡(luò)編程和 TCP 抓包實(shí)操》一文中闡述的 Go 網(wǎng)絡(luò)編程模型,就是一種異步系統(tǒng)調(diào)用。它使用網(wǎng)路輪詢器進(jìn)行系統(tǒng)調(diào)用,調(diào)度器可以防止 G 在進(jìn)行這些系統(tǒng)調(diào)用時(shí)阻塞 M。這可以讓 M 繼續(xù)執(zhí)行其他的 G,而不需要?jiǎng)?chuàng)建新的 M。

但是,如果 G 要進(jìn)行的是無(wú)法異步完成的系統(tǒng)調(diào)用時(shí)怎么辦?當(dāng)網(wǎng)絡(luò)輪詢器無(wú)法使用時(shí),進(jìn)行系統(tǒng)調(diào)用的 G 將會(huì)阻塞 M。在 Linux 下基于普通文件(Linux 下的 epoll 只支持 socket,Windows 下的 iocp 可以支持 socket、file)的系統(tǒng)調(diào)用就是一個(gè)典型的例子。

同步系統(tǒng)調(diào)用 1

如上圖所示,運(yùn)行在 M1 上的 G1 想要請(qǐng)求一個(gè)同步系統(tǒng)調(diào)用。

同步系統(tǒng)調(diào)用 2

當(dāng)發(fā)生同步系統(tǒng)調(diào)用并阻塞時(shí),調(diào)度器將 M1 和仍然掛載在其之上的 G1 與 P 分離,并引入新的 M2 來(lái)運(yùn)行 P 上的其他 G。

同步系統(tǒng)調(diào)用 3

當(dāng) G1 進(jìn)行的阻塞式系統(tǒng)調(diào)用結(jié)束時(shí),G1 重新回到 P 的 LRQ 中去,但 M1 變成了閑置線程,不會(huì)被回收,以留備后續(xù)復(fù)用。

問(wèn)題來(lái)了,如果在某一短時(shí)段內(nèi),Go 程序存在大量無(wú)法短暫結(jié)束的同步系統(tǒng)調(diào)用,那線程數(shù)豈不是會(huì)一直漲下去?

最大線程數(shù)限制

線程數(shù)限制的問(wèn)題,在官方 issues#4056: "runtime: limit number of operating system threads" 中,有過(guò)討論,并最終將線程限制數(shù)值確定為 10000。

這個(gè)值存在的主要目的是限制可以創(chuàng)建無(wú)限數(shù)量線程的 Go 程序:在程序把操作系統(tǒng)干爆之前,干掉程序。

當(dāng)然,Go 也暴露了 debug.SetMaxThreads() 方法可以讓我們修改最大線程數(shù)值。

  1. package main 
  2.  
  3. import ( 
  4.  "os/exec" 
  5.  "runtime/debug" 
  6.  "time" 
  7.  
  8. func main() { 
  9.  debug.SetMaxThreads(10) 
  10.  for i := 0; i < 20; i++ { 
  11.   go func() { 
  12.    _, err := exec.Command("bash""-c""sleep 3").Output() 
  13.    if err != nil { 
  14.     panic(err) 
  15.    } 
  16.   }() 
  17.  } 
  18.  time.Sleep(time.Second * 5) 

如程序所示,我們將最大線程數(shù)設(shè)置為 10,然后通過(guò)執(zhí)行 shell 命令 sleep 3 來(lái)模擬同步系統(tǒng)調(diào)用過(guò)程。那么,執(zhí)行 sleep 操作的 G 和 M 都會(huì)阻塞,當(dāng)程序啟動(dòng)的線程 M 超過(guò) 10 個(gè)時(shí),會(huì)得到以下報(bào)錯(cuò)。

  1. runtime: program exceeds 10-thread limit 
  2. fatal error: thread exhaustion 
  3. *** 

讓閑置線程退出

閑置線程退出的問(wèn)題,在官方 issues#14592: "runtime: let idle OS threads exit" 中有過(guò)討論。目前,還沒(méi)有一個(gè)完美的解決方案。

但是,在該 issue 里有人提出使用 runtime.LockOSThread() 方法來(lái)殺死線程。

簡(jiǎn)單了解下這個(gè)函數(shù)的特性:

  • 調(diào)用 LockOSThread 函數(shù)會(huì)把當(dāng)前 G 綁定在當(dāng)前的系統(tǒng)線程 M 上,這個(gè) G 總是在這個(gè) M 上執(zhí)行,并且阻止其它 G 在該 M 執(zhí)行。
  • 只有當(dāng)前 G 調(diào)用了與之前調(diào)用 LockOSThread 相同次數(shù)的 UnlockOSThread 函數(shù)之后,G 與 M 才會(huì)解綁。
  • 如果當(dāng)前 G 在退出時(shí),沒(méi)有調(diào)用 UnlockOSThread,這個(gè)線程會(huì)被終止。

那么,我們可以利用第三個(gè)特性,在啟動(dòng) G 時(shí),調(diào)用 LockOSThread 來(lái)獨(dú)占一個(gè) M。當(dāng) G 退出時(shí),而不調(diào)用 UnlockOSThread,那這個(gè) M 將不會(huì)被閑置,就被終止了。

下面,我們來(lái)看一個(gè)例子

  1. package main 
  2.  
  3. import ( 
  4.  "fmt" 
  5.  "os/exec" 
  6.  "runtime/pprof" 
  7.  "time" 
  8.  
  9. func main() { 
  10.  threadProfile := pprof.Lookup("threadcreate"
  11.  fmt.Printf(" init threads counts: %d\n", threadProfile.Count()) 
  12.  
  13.  for i := 0; i < 20; i++ { 
  14.   go func() { 
  15.    _, err := exec.Command("bash""-c""sleep 3").Output() 
  16.    if err != nil { 
  17.     panic(err) 
  18.    } 
  19.   }() 
  20.  } 
  21.  time.Sleep(time.Second * 5) 
  22.  fmt.Printf(" end threads counts: %d\n", threadProfile.Count()) 

通過(guò) threadProfile.Count() 我們可以實(shí)時(shí)獲取當(dāng)前線程數(shù)目,那么在發(fā)生了阻塞式系統(tǒng)調(diào)用后,該程序的線程數(shù)目是多少呢?

  1. init threads counts: 5 
  2. end threads counts: 25 

根據(jù)結(jié)果可以看到,G 執(zhí)行完畢后,閑置線程并沒(méi)有被釋放。

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

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

此時(shí),程序的執(zhí)行結(jié)果如下

  1. init threads counts: 5 
  2.  
  3. end threads counts: 11 

可以看到,由于調(diào)用了 LockOSThread 函數(shù)的 G 沒(méi)有執(zhí)行 UnlockOSThread 函數(shù),在 G 執(zhí)行完畢后,M 也被終止了。

總結(jié)

在 GMP 模型中,P 與 M 一對(duì)一的掛載形式,通過(guò)設(shè)定 GOMAXPROCS 變量就能控制并行線程數(shù)。

當(dāng) M 遇到同步系統(tǒng)調(diào)用時(shí),G 和 M 會(huì)與 P 剝離,當(dāng)系統(tǒng)調(diào)用完成,G 重新進(jìn)入可運(yùn)行狀態(tài),而 M 就會(huì)被閑置起來(lái)。

Go 目前并沒(méi)有對(duì)閑置線程做清除處理,它們被當(dāng)作復(fù)用的資源,以備后續(xù)需要。但是,如果在 Go 程序中積累大量空閑線程,這是對(duì)資源的一種浪費(fèi),同時(shí)對(duì)操作系統(tǒng)也產(chǎn)生了威脅。因此,Go 設(shè)定了 10000 的默認(rèn)線程數(shù)限制。

我們發(fā)現(xiàn)了一種利用 LockOSThread 函數(shù)的 trik 做法,可以借此做一些限制線程數(shù)的方案:例如啟動(dòng)定期排查線程數(shù)的 goroutine,當(dāng)發(fā)現(xiàn)程序的線程數(shù)超過(guò)某閾值后,就回收掉一部分閑置線程。

當(dāng)然,這個(gè)方法也存在隱患。例如在 issues#14592 有人提到:當(dāng)子進(jìn)程由一個(gè)帶有 PdeathSignal: SIGKILL 的 A 線程創(chuàng)建,A 變?yōu)榭臻e時(shí),如果 A 退出,那么子進(jìn)程將會(huì)收到 KILL 信號(hào),從而引起其他問(wèn)題。

當(dāng)然,絕大多數(shù)情況下,我們的 Go 程序并不會(huì)遇到空閑線程數(shù)過(guò)多的問(wèn)題。如果真的存在線程數(shù)暴漲的問(wèn)題,那么你應(yīng)該思考代碼邏輯是否合理(為什么你能允許短時(shí)間內(nèi)如此多的系統(tǒng)同步調(diào)用),是否可以做一些例如限流之類(lèi)的處理。而不是想著通過(guò) SetMaxThreads 方法來(lái)處理。

參考

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

 

責(zé)任編輯:武曉燕 來(lái)源: Golang技術(shù)分享
相關(guān)推薦

2023-10-29 16:14:07

2012-04-23 14:36:10

天璣科技IT成本IT服務(wù)

2010-05-31 16:46:31

2009-01-21 18:31:50

2011-09-06 10:33:11

2023-12-29 08:10:41

Go并發(fā)開(kāi)發(fā)

2017-12-18 10:27:30

數(shù)據(jù)中心制冷系統(tǒng)

2014-11-17 10:05:12

Go語(yǔ)言

2023-12-01 08:01:33

GoValidator

2024-04-30 10:29:46

前端開(kāi)發(fā)h5開(kāi)發(fā)函數(shù)

2020-07-16 14:25:18

PythonGo前端

2024-02-04 19:38:02

云計(jì)算

2010-03-16 17:52:27

Java多線程信號(hào)量

2022-06-20 10:45:55

SpringBoot項(xiàng)目

2022-12-27 09:57:41

線程數(shù)CPU

2021-03-29 17:51:00

瑞數(shù)信息攻防演練

2021-06-25 15:19:13

攻防演練

2024-05-08 00:00:00

核心線程數(shù)隊(duì)列

2024-06-17 08:40:16

2023-01-30 15:41:10

Channel控制并發(fā)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)