被問懵了:一個程式最多可以建立多少個執行緒?

小林coding發表於2021-07-15

大家好,我是小林。

昨天有位讀者問了我這麼個問題:


大致意思就是,他看了一個面經,說虛擬記憶體是 2G 大小,然後他看了我的圖解系統 PDF 裡說虛擬記憶體是 4G,然後他就懵逼了。

其實他看這個面經很有問題,沒有說明是什麼作業系統,以及是多少位作業系統。

因為不同的作業系統和不同位數的作業系統,虛擬記憶體可能是不一樣多。

Windows 系統我不瞭解,我就說說 Linux 系統。

在 Linux 作業系統中,虛擬地址空間的內部又被分為核心空間和使用者空間兩部分,不同位數的系統,地址 空間的範圍也不同。比如最常⻅的 32 位和 64 位系統,如下所示:

通過這裡可以看出:

  • 32 位系統的核心空間佔用 1G ,位於最高處,剩下的 3G 是使用者空間;
  • 64 位系統的核心空間和使用者空間都是 128T ,分別佔據整個記憶體空間的最高和最低處,剩下的中
    間部分是未定義的。

接著,來看看讀者那個面經題目:一個程式最多可以建立多少個執行緒?

這個問題跟兩個東西有關係:

  • 程式的虛擬記憶體空間上限,因為建立一個執行緒,作業系統需要為其分配一個棧空間,如果執行緒數量越多,所需的棧空間就要越大,那麼虛擬記憶體就會佔用的越多。
  • 系統引數限制,雖然 Linux 並沒有核心引數來控制單個程式建立的最大執行緒個數,但是有系統級別的引數來控制整個系統的最大執行緒個數。

我們先看看,在程式裡建立一個執行緒需要消耗多少虛擬記憶體大小?

我們可以執行 ulimit -a 這條命令,檢視程式建立執行緒時預設分配的棧空間大小,比如我這臺伺服器預設分配給執行緒的棧空間大小為 8M。

在前面我們知道,在 32 位 Linux 系統裡,一個程式的虛擬空間是 4G,核心分走了1G,留給使用者用的只有 3G

那麼假設建立一個執行緒需要佔用 10M 虛擬記憶體,總共有 3G 虛擬記憶體可以使用。於是我們可以算出,最多可以建立差不多 300 個(3G/10M)左右的執行緒。

如果你想自己做個實驗,你可以找臺 32 位的 Linux 系統執行下面這個程式碼:

由於我手上沒有 32 位的系統,我這裡貼一個網上別人做的測試結果:

如果想使得程式建立上千個執行緒,那麼我們可以調整建立執行緒時分配的棧空間大小,比如調整為 512k:

$ ulimit -s 512

說完 32 位系統的情況,我們來看看 64 位系統裡,一個程式能建立多少執行緒呢?

我的測試伺服器的配置:

  • 64 位系統;
  • 2G 實體記憶體;
  • 單核 CPU。

64 位系統意味著使用者空間的虛擬記憶體最大值是 128T,這個數值是很大的,如果按建立一個執行緒需佔用 10M 棧空間的情況來算,那麼理論上可以建立 128T/10M 個執行緒,也就是 1000多萬個執行緒,有點魔幻!

所以按 64 位系統的虛擬記憶體大小,理論上可以建立無數個執行緒。

事實上,肯定建立不了那麼多執行緒,除了虛擬記憶體的限制,還有系統的限制。

比如下面這三個核心引數的大小,都會影響建立執行緒的上限:

  • /proc/sys/kernel/threads-max,表示系統支援的最大執行緒數,預設值是 14553
  • /proc/sys/kernel/pid_max,表示系統全域性的 PID 號數值的限制,每一個程式或執行緒都有 ID,ID 的值超過這個數,程式或執行緒就會建立失敗,預設值是 32768
  • /proc/sys/vm/max_map_count,表示限制一個程式可以擁有的VMA(虛擬記憶體區域)的數量,具體什麼意思我也沒搞清楚,反正如果它的值很小,也會導致建立執行緒失敗,預設值是 65530

那接下針對我的測試伺服器的配置,看下一個程式最多能建立多少個執行緒呢?

我在這臺伺服器跑了前面的程式,其結果如下:

可以看到,建立了 14374 個執行緒後,就無法在建立了,而且報錯是因為資源的限制。

前面我提到的 threads-max 核心引數,它是限制系統裡最大執行緒數,預設值是 14553。

我們可以執行那個測試執行緒數的程式後,看下當前系統的執行緒數是多少,可以通過 top -H 檢視。

左上角的 Threads 的數量顯示是 14553,與 threads-max 核心引數的值相同,所以我們可以認為是因為這個引數導致無法繼續建立執行緒。

那麼,我們可以把 threads-max 引數設定成 99999:

echo 99999 > /proc/sys/kernel/threads-max

設定完 threads-max 引數後,我們重新跑測試執行緒數的程式,執行後結果如下圖:

可以看到,當程式建立了 32326 個執行緒後,就無法繼續建立裡,且報錯是無法繼續申請記憶體。

此時的上限個數很接近 pid_max 核心引數的預設值(32768),那麼我們可以嘗試將這個引數設定為 99999:

echo 99999 > /proc/sys/kernel/pid_max

設定完 pid_max 引數後,繼續跑測試執行緒數的程式,執行後結果建立執行緒的個數還是一樣卡在了 32768 了。

當時我也挺疑惑的,明明 pid_max 已經調整大後,為什麼執行緒個數還是上不去呢?

後面經過查閱資料發現,max_map_count 這個核心引數也是需要調大的,但是它的數值與最大執行緒數之間有什麼關係,我也不太明白,只是知道它的值是會限制建立執行緒個數的上限。

然後,我把 max_map_count 核心引數也設定成後 99999:

echo 99999 > /proc/sys/kernel/max_map_count 

繼續跑測試執行緒數的程式,結果如下圖:

當建立差不多 5 萬個執行緒後,我的伺服器就卡住不動了,CPU 都已經被佔滿了,畢竟這個是單核 CPU,所以現在是 CPU 的瓶頸了。

我只有這臺伺服器,如果你們有效能更強的伺服器來測試的話,有興趣的小夥伴可以去測試下。

接下來,我們換個思路測試下,把建立執行緒時分配的棧空間調大,比如調大為 100M,在大就會建立執行緒失敗。

ulimit -s 1024000

設定完後,跑測試執行緒的程式,其結果如下:

總共建立了 26390 個執行緒,然後就無法繼續建立了,而且該程式的虛擬記憶體空間已經高達 25T,要知道這臺伺服器的實體記憶體才 2G。

為什麼實體記憶體只有 2G,程式的虛擬記憶體卻可以使用 25T 呢?

因為虛擬記憶體並不是全部都對映到實體記憶體的,程式是有區域性性的特性,也就是某一個時間只會執行部分程式碼,所以只需要對映這部分程式就好。

你可以從上面那個 top 的截圖看到,雖然程式虛擬空間很大,但是實體記憶體(RES)只有使用了 400 多M。

好了,簡單總結下:

  • 32 位系統,使用者態的虛擬空間只有 3G,如果建立執行緒時分配的棧空間是 10M,那麼一個程式最多隻能建立 300 個左右的執行緒。
  • 64 位系統,使用者態的虛擬空間大到有 128T,理論上不會受虛擬記憶體大小的限制,而會受系統的引數或效能限制。

絮叨絮叨

小林在 CSDN 寫了很多圖解網路和作業系統的系列文章,很高興收穫到很朋友的認可和支援,正好最近圖解網路和作業系統的文章連載的有 20+ 篇了,也算有個體系了。

在這裡插入圖片描述

所以為了方便大家閱讀,小林把自己原創的圖解網路和圖解作業系統整理成了 PDF,一整理後,沒想到每個圖解都輸出了 15 萬字 + 500 張圖,質量也是槓槓的,有很多朋友特地私信我,看了我的圖解拿到了大廠的offer。

圖解系統 PDF 開源下載:圖解系統 PDF 下載地址(點選)

圖解網路 PDF 開源下載:圖解網路 PDF 下載地址(點選)


我是小林,今天的你,比昨天更博學了嗎?

相關文章