切換語言為:簡體
利用實踐案例說明「多執行緒配置多少執行緒數最合適」

利用實踐案例說明「多執行緒配置多少執行緒數最合適」

  • 爱糖宝
  • 2024-07-13
  • 2066
  • 0
  • 0

執行緒到底設定數量多少合適

網上有很多文章說設定執行緒數的理論(個人不信):

  1. CPU密集型的程式 - 核心數 + 1

  2. I/O密集型的程式 - 核心數 * 2

CPU利用率

在分析執行緒數之前,先說一個基本的理論

一個CPU核心,單位時間內只能執行一個執行緒的指令

那麼理論上,我一個執行緒只需要不停的執行指令,就可以跑滿一個核心的利用率。

來寫個死迴圈空跑的例子驗證一下:

public class CPUUtilizationTest{
   public static void main(String[] args){
     //死迴圈,什麼都不做
     while(true){
     }
}}

執行這個例子後,來看看現在CPU的利用率:

利用實踐案例說明「多執行緒配置多少執行緒數最合適」

從圖上可以看到,我的3號核心利用率已經被跑滿了

那基於上面的理論,我多開幾個執行緒試試呢?

public class CPUUtilizationTest{
     public static void main(String[] args){
         for(int j =0; j <6; j++){
         new Thread(newRunnable(){
             @Override
             public void run(){
                 while(true){
                 }
             }
         }).start();
     }
}}

此時再看CPU利用率,1/2/5/7/9/11 幾個核心的利用率已經被跑滿:

利用實踐案例說明「多執行緒配置多少執行緒數最合適」

那如果開12個執行緒呢,是不是會把所有核心的利用率都跑滿?答案一定是會的:

利用實踐案例說明「多執行緒配置多少執行緒數最合適」

如果此時我把上面例子的執行緒數繼續增加到24個執行緒,會出現什麼結果呢?

CPU利用率和上一步一樣,還是所有核心100%,不過此時負載從11.x增加到了22.x,說明此時CPU更繁忙,執行緒的任務無法及時執行。

現代CPU基本都是多核心的,我們可以簡單的認為是12核心CPU。那麼我這個CPU就可以同時做12件事,互不打擾。

如果要執行的執行緒大於核心數,那麼就需要透過作業系統的排程了。作業系統給每個執行緒分配CPU時間片資源,然後不停的切換,從而實現“並行”執行的效果。

但是這樣真的更快嗎?從上面的例子可以看出,一個執行緒就可以把一個核心的利用率跑滿。如果每個執行緒都很“霸道”,不停的執行指令,不給CPU空閒的時間,並且同時執行的執行緒數大於CPU的核心數,就會導致作業系統更頻繁的執行切換執行緒執行,以確保每個執行緒都可以得到執行。

不過切換是有代價的,每次切換會伴隨著暫存器資料更新,記憶體頁表更新等操作。雖然一次切換的代價和I/O操作比起來微不足道,但如果執行緒過多,執行緒切換的過於頻繁,甚至在單位時間內切換的耗時已經大於程式執行的時間,就會導致CPU資源過多的浪費在上下文切換上,而不是在執行程式,得不償失。


上面死迴圈空跑的例子,有點過於極端了,正常情況下不太可能有這種程式。

大多程式在執行時都會有一些 I/O操作,可能是讀寫檔案,網路收發報文等,這些 I/O 操作在進行時時需要等待反饋的。比如網路讀寫時,需要等待報文傳送或者接收到,在這個等待過程中,執行緒是等待狀態,CPU沒有工作。此時作業系統就會排程CPU去執行其他執行緒的指令,這樣就完美利用了CPU這段空閒期,提高了CPU的利用率。

上面的例子中,程式不停的迴圈什麼都不做,CPU要不停的執行指令,幾乎沒有啥空閒的時間。如果插入一段I/O操作呢,I/O 操作期間 CPU是空閒狀態,CPU的利用率會怎麼樣呢?先看看單執行緒下的結果:

public class CPUUtilizationTest{
     public static void main(String[] args)throws InterruptedException{
         for(int n =0; n <1; n++){
         new Thread(newRunnable(){
             @Override
             public void run(){
                 while(true){
                     //每次空迴圈 1億 次後,sleep 50ms,模擬 I/O等待、切換
                     for(int i =0; i <100_000_000l; i++){ 
                     }
                     try{
                     		Thread.sleep(50);
                     }
                     catch(InterruptedException e){
                     		e.printStackTrace();
                     }
                 }
             }
         }).start();
     }
}}

利用實踐案例說明「多執行緒配置多少執行緒數最合適」

唯一有利用率的9號核心,利用率也才50%,和前面沒有sleep的100%相比,已經低了一半了。現在把執行緒數調整到12個看看:

利用實踐案例說明「多執行緒配置多少執行緒數最合適」

單個核心的利用率60左右,和剛纔的單執行緒結果差距不大,還沒有把CPU利用率跑滿,現在將執行緒數增加到18:

利用實踐案例說明「多執行緒配置多少執行緒數最合適」

此時單核心利用率,已經接近100%了。由此可見,當執行緒中有 I/O 等操作不佔用CPU資源時,作業系統可以排程CPU可以同時執行更多的執行緒。

現在將I/O事件的頻率調高看看呢,把迴圈次數減到一半,50_000_000,同樣是18個執行緒,此時每個核心的利用率,大概只有70%左右了。

執行緒數和CPU利用率總結

上面的例子,只是輔助,爲了更好的理解執行緒數/程式行為/CPU狀態的關係,來簡單總結一下:

  1. 一個極端的執行緒(不停執行“計算”型操作時),就可以把單個核心的利用率跑滿,多核心CPU最多隻能同時執行等於核心數的“極端”執行緒數

  2. 如果每個執行緒都這麼“極端”,且同時執行的執行緒數超過核心數,會導致不必要的切換,造成負載過高,只會讓執行更慢

  3. I/O 等暫停類操作時,CPU處於空閒狀態,作業系統排程CPU執行其他執行緒,可以提高CPU利用率,同時執行更多的執行緒

  4. I/O 事件的頻率越高,或者等待/暫停時間越長,CPU的空閒時間也就更長,利用率越低,作業系統可以排程CPU執行更多的執行緒

執行緒數規劃的公式總結

在《Java 併發程式設計實戰》中介紹了一個執行緒數計算的公式

利用實踐案例說明「多執行緒配置多少執行緒數最合適」 

如果希望程式跑到CPU的目標利用率,需要的執行緒數公式為:

利用實踐案例說明「多執行緒配置多少執行緒數最合適」

公式很清晰,現在來帶入上面的例子試試看:

如果我期望目標利用率為90%(多核90),那麼需要的執行緒數為:

核心數12 * 利用率0.9 * (1 + 50(sleep時間)/50(迴圈50_000_000耗時)) ≈ 22

分析一下,W是等待時間比如IO等待的時間,C是計算時間即我完成那麼多次迴圈一共耗時是多少

現在把執行緒數調到22,看看結果:

利用實踐案例說明「多執行緒配置多少執行緒數最合適」

現在CPU利用率大概80+,和預期比較接近了,由於執行緒數過多,還有些上下文切換的開銷,再加上測試用例不夠嚴謹,所以實際利用率低一些也正常。

真實程式中的執行緒數是怎樣的

那麼在實際的程式中,或者說一些Java的業務系統中,執行緒數(執行緒池大小)規劃多少合適呢?

先說結論:沒有固定答案,先設定預期,比如我期望的CPU利用率在多少,負載在多少,GC頻率多少之類的指標後,再透過測試不斷的調整到一個合理的執行緒數

比如一個普通的,SpringBoot 為基礎的業務系統,預設Tomcat容器+連線池+垃圾回收器,如果此時專案中也需要一個業務場景的多執行緒(或者執行緒池)來非同步/並行執行業務流程。

此時我按照上面的公式來規劃執行緒數的話,誤差一定會很大因為此時這臺主機上,已經有很多執行中的執行緒了,Tomcat有自己的執行緒池,JVM也有一些執行緒,垃圾回收器都有自己的後臺執行緒。這些執行緒也是執行在當前程序、當前主機上的,也會佔用CPU的資源。

所以受環境干擾下,單靠公式很難準確的規劃執行緒數,一定要透過測試來驗證。

流程一般是這樣:

  1. 分析當前主機上,有沒有其他程序干擾

  2. 分析當前JVM程序上,有沒有其他執行中或可能執行的執行緒

  3. 設定目標

  4. 目標CPU利用率 - 我最高能容忍我的CPU飆到多少?

  5. 目標GC頻率/暫停時間 - 多執行緒執行後,GC頻率會增高,最大能容忍到什麼頻率,每次暫停時間多少?

  6. 執行效率 - 比如批處理時,我單位時間內要開多少執行緒才能及時處理完畢

  7. ……

所以,不要糾結設定多少執行緒了。沒有標準答案,一定要結合場景,帶著目標,透過測試去找到一個最合適的執行緒數。

可能會有疑問:“我們系統也沒啥壓力,不需要那麼合適的執行緒數,只是一個簡單的非同步場景,不影響系統其他功能就可以”

很正常,不需要啥效能,穩定好用符合需求就可以了。那麼我的推薦的執行緒數是:CPU核心數

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.