別再糾結線程池池大小、線程數量了,哪有什麼固定公式

来源:https://www.cnblogs.com/jingdongkeji/archive/2023/09/20/17716640.html
-Advertisement-
Play Games

可能很多人都看到過一個線程數設置的理論:CPU 密集型的程式 - 核心數 + 1,I/O 密集型的程式 - 核心數 * 2,不會吧,不會吧,真的有人按照這個理論規劃線程數? ...


可能很多人都看到過一個線程數設置的理論:

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

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

不會吧,不會吧,真的有人按照這個理論規劃線程數?

線程數和CPU利用率的小測試

拋開一些操作系統,電腦原理不談,說一個基本的理論(不用糾結是否嚴謹,只為好理解):一個CPU核心,單位時間內只能執行一個線程的指令

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

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

測試環境:AMD Ryzen 5 3600, 6 - Core, 12 - Threads

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


運行這個例子後,來看看現在CPU的利用率:
image.png
從圖上可以看到,我的3號核心利用率已經被跑滿了

那基於上面的理論,我多開幾個線程試試呢?

public class CPUUtilizationTest {
	public static void main(String[] args) {

		for (int j = 0; j < 6; j++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					while (true){
					}
				}
			}).start();
		}
	}
}


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

那如果開12個線程呢,是不是會把所有核心的利用率都跑滿?答案一定是會的
image.png

如果此時我把上面例子的線程數繼續增加到24個線程,會出現什麼結果呢?
image.png

從上圖可以看到,CPU利用率和上一步一樣,還是所有核心100%,不過此時負載已經從11.x增加到了22.x(load average解釋參考https://scoutapm.com/blog/understanding-load-averages),說明此時CPU更繁忙,線程的任務無法及時執行。

現代CPU基本都是多核心的,比如我這裡測試用的AMD 3600,6核心12線程(超線程),我們可以簡單的認為它就是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(new Runnable() {
				@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();
		}
	}
}


image.png

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

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

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

現在將I/O事件的頻率調高看看呢,把迴圈次數減到一半,50_000_000,同樣是18個線程:

image.png

此時每個核心的利用率,大概只有70%左右了。

線程數和CPU利用率的小總結

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

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

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

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

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

線程數規劃的公式

前面的鋪墊,都是為了幫助理解,現在來看看書本上的定義。《Java 併發編程實戰》介紹了一個線程數計算的公式:

CleanShot 2023-09-07 at 12.41.41@2x.png

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

CleanShot 2023-09-07 at 12.42.02@2x.png

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

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

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

現在把線程數調到22,看看結果:
image.png

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

把公式變個形,還可以通過線程數來計算CPU利用率:

CleanShot 2023-09-07 at 12.41.11@2x.png

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

雖然公式很好,但在真實的程式中,一般很難獲得準確的等待時間和計算時間,因為程式很複雜,不只是“計算”。一段代碼中會有很多的記憶體讀寫,計算,I/O 等複合操作,精確的獲取這兩個指標很難,所以光靠公式計算線程數過於理想化。

真實程式中的線程數

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

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

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

此時我按照上面的公式來規劃線程數的話,誤差一定會很大。因為此時這台主機上,已經有很多運行中的線程了,Tomcat有自己的線程池,HikariCP也有自己的後臺線程,JVM也有一些編譯的線程,連G1都有自己的後臺線程。這些線程也是運行在當前進程、當前主機上的,也會占用CPU的資源。

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

流程一般是這樣:

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

  2. 分析當前JVM進程上,有沒有其他運行中或可能運行的線程

  3. 設定目標

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

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

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

    4. ……

  4. 梳理鏈路關鍵點,是否有卡脖子的點,因為如果線程數過多,鏈路上某些節點資源有限可能會導致大量的線程在等待資源(比如三方介面限流,連接池數量有限,中間件壓力過大無法支撐等)

  5. 不斷的增加/減少線程數來測試,按最高的要求去測試,最終獲得一個“滿足要求”的線程數**

而且而且而且!不同場景下的線程數理念也有所不同:

  1. Tomcat中的maxThreads,在Blocking I/O和No-Blocking I/O下就不一樣

  2. Dubbo 預設還是單連接呢,也有I/O線程(池)和業務線程(池)的區分,I/O線程一般不是瓶頸,所以不必太多,但業務線程很容易稱為瓶頸

  3. Redis 6.0以後也是多線程了,不過它只是I/O 多線程,“業務”處理還是單線程

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

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

很正常,很多的內部業務系統,並不需要啥性能,穩定好用符合需求就可以了。那麼我的推薦的線程數是:CPU核心數

附錄

Java 獲取CPU核心數

Runtime.getRuntime().availableProcessors()//獲取邏輯核心數,如6核心12線程,那麼返回的是12


Linux 獲取CPU核心數

# 總核數 = 物理CPU個數 X 每顆物理CPU的核數 
# 總邏輯CPU數 = 物理CPU個數 X 每顆物理CPU的核數 X 超線程數

# 查看物理CPU個數
cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l

# 查看每個物理CPU中core的個數(即核數)
cat /proc/cpuinfo| grep "cpu cores"| uniq

# 查看邏輯CPU的個數
cat /proc/cpuinfo| grep "processor"| wc -l


如果我的文章對您有幫助,請點贊/收藏/關註鼓勵支持一下吧❤❤❤❤❤❤

作者:京東保險 蔣信

來源:京東雲開發者社區 轉載請註明來源


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 大家好,我是Antvictor,一個勵志要成為架構師的程式員。 閑話少說,讓我們直接開始安裝Python。 Python安裝 從Python官網找到Download下載對應的安裝包,python3.6及以上即可。 Python官網會根據系統預設展示對應系統的最新版本安裝包,下載成功後點擊安裝。 這裡 ...
  • Python庫解析地址PyParsing 人們普遍認為,Python編程語言的pyparsing 模塊是對文本數據進行操作的一個寶貴工具。 用於解析和修改文本數據的pyparsing 包,簡化了對地址的操作。這是因為該模塊可以轉換和幫助解析地址。 在這篇文章中,我們將討論PyParsing 模塊在處 ...
  • 所有的面試題目都不是一成不變的,面試題目只是給大家一個借鑒作用,最主要的是給自己增加知識的儲備,有備無患。 ...
  • 大家好,我是 Java陳序員,今天給大家介紹一個顏值功能雙線上的 Zookeeper 可視化工具。 項目介紹 PrettyZoo 是一個基於 Apache Curator 和 JavaFX 實現的 Zookeeper 圖形化管理客戶端。 使用了 Java 的模塊化(Jigsaw)技術,並基於 JPa ...
  • 2.1、環境搭建 2.1.1、右擊project創建新module 2.1.2、選擇maven 2.1.3、設置module名稱和路徑 2.1.4、module初始狀態 2.1.5、配置打包方式 註意:預設的打包方式為 jar,為了能配置web資源,需要將打包方式設置為 war <packaging ...
  • 在之前的Java 17新特性中,我們介紹過關於JEP 406: switch的模式匹配,但當時還只是關於此內容的首個預覽版本。之後在JDK 18、JDK 19、JDK 20中又都進行了更新和完善。如今,在JDK 21中,該特性得到了最終確定!下麵,我們就再正式學習一下該功能! 在以往的switch語 ...
  • 本文介紹了Python 非同步編程技術asyncio ,使用場景,介紹了同步編程,非同步編程原理,非同步技術的優勢,非同步語法 async await, 協程,create_task, gather, event loop, asyncio.run() 等,用回調函數callback 來解析響應消息,實... ...
  • 全局有序 在RocketMQ中,如果使消息全局有序,可以為Topic設置一個消息隊列,使用一個生產者單線程發送數據,消費者端也使用單線程進行消費,從而保證消息的全局有序,但是這種方式效率低,一般不使用。 局部有序 假設一個Topic分配了兩個消息隊列,生產者在發送消息的時候,可以對消息設置一個路由I ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...