聽我一句勸,業務程式碼中,別用多執行緒。
來源:why技術
你好呀,我是歪歪。
前幾天我在網上衝浪,看到一個哥們在吐槽,說他工作三年多了,沒使用過多執行緒。
雖然八股文背的滾瓜爛熟,但是沒有在實際開發過程中寫的都是業務程式碼,沒有使用過執行緒池,心裡還是慌得一比。
我只是微微一笑,這不是很正常嗎?
業務程式碼中一般也使不上多執行緒,或者說,業務程式碼中不知不覺你以及在使用執行緒池了,你再 duang 的一下搞一個出來,反而容易出事。
所以提到執行緒池的時候,我個人的觀點是必須把它吃得透透的,但是在業務程式碼中少用或者不用多執行緒。
關於這個觀點,我給你盤一下。
Demo
首先我們還是花五分鐘搭個 Demo 出來。
我手邊剛好有一個之前搭的一個關於 Dubbo 的 Demo,消費者、生產者都有,我就直接拿來用了:
這個 Demo 我也是跟著網上的 quick start 搞的:
可以說寫的非常詳細了,你就跟著官網的步驟一步步的搞就行了。
我這個 Demo 稍微不一樣的是我在消費者模組裡面搞了一個 Http 介面:
在介面裡面發起了 RPC 呼叫,模擬從前端頁面發起請求的場景,更加符合我們的開發習慣。
而官方的示例中,是基於了 SpringBoot 的 CommandLineRunner 去發起呼叫:
只是發起呼叫的方式不一樣而已,其他沒啥大區別。
需要說明的是,我只是手邊剛好有一個 Dubbo 的 Demo,隨手就拿來用了,但是本文想要表達的觀點,和你使不使用 Dubbo 作為 RPC 框架,沒有什麼關係,道理是通用的。
上面這個 Demo 啟動起來之後,透過 Http 介面發起一次呼叫,看到控制檯服務提供方和服務消費方都有對應的日誌輸出,準備工作就算是齊活兒了:
上菜
在上面的 Demo 中,這是消費者的程式碼:
這是提供者的程式碼:
整個呼叫鏈路非常的清晰:
來,請你告訴我這裡面有執行緒池嗎?
沒有!
是的,在日常的開發中,我就是寫個介面給別人呼叫嘛,在我的介面裡面並沒有執行緒池相關的程式碼,只有 CRUD 相關的業務程式碼。
同時,在日常的開發中,我也經常呼叫別人提供給我的介面,也是一把梭,擼到底,根本就不會用到執行緒池。
所以,站在我,一個開發人員的角度,這個裡面沒有執行緒池。
合理,非常合理。
但是,當我們換個角度,再看看,它也是可以有的。
比如這樣:
反應過來沒有?
我們發起一個 Http 呼叫,是由一個 web 容器來處理這個請求的,你甭管它是 Tomcat,還是 Jetty、Netty、Undertow 這些玩意,反正是個 web 容器在處理。
那你說,這個裡面有執行緒池嗎?
在方法入口處打個斷點,這個 http-nio-8081-exec-1 不就是 Tomcat 容器執行緒池裡面的一個執行緒嗎:
透過 dump 堆疊資訊,過濾關鍵字可以看到這樣的執行緒,在服務啟動起來,啥也沒幹的情況下,一共有 10 個:
朋友,這不就是執行緒池嗎?
雖然不是你寫的,但是你確實用了。
我寫出來的這個 test 介面,就是會由 web 容器中的一個執行緒來進行呼叫。所以,站在 web 容器的角度,這裡是有一個執行緒池的:
同理,在 RPC 框架中,不管是消費方,還是服務提供方,也都存在著執行緒池。
比如 Dubbo 的執行緒池,你可以看一下官方的文件:
而對於大多數的框架來說,它絕不可能只有一個執行緒池,為了做資源隔離,它會啟用好幾個執行緒池,達到執行緒池隔離,互不干擾的效果。
比如參與 Dubbo 一次呼叫的其實不僅一個執行緒池,至少還有 IO 執行緒池和業務執行緒池,它們各司其職:
我們主要關注這個業務執行緒池。
反正站在 Dubbo 框架的角度,又可以補充一下這個圖片了:
那麼問題來了,在當前的這個情況下?
當有人反饋:哎呀,這個服務吞吐量怎麼上不去啊?
你怎麼辦?
你會 duang 的一下在業務邏輯裡面加一個執行緒池嗎?
大哥,前面有個 web 容器的執行緒池,後面有個框架的執行緒池,兩頭不調整,你在中間加個執行緒池,加它有啥用啊?
web 容器,拿 Tomcat 來說,人家給你提供了執行緒池引數調整的相關配置,這麼一大坨配置,你得用起來啊:
https://tomcat.apache.org/tomcat-9.0-doc/config/executor.html
再比如 Dubbo 框架,都給你明說了,這些引數屬於效能調優的範疇,感覺不對勁了,你先動手調調啊:
你把這些引數調優弄好了,絕對比你直接懟個執行緒池在業務程式碼中,效果好的多。
甚至,你在業務程式碼中加入一個執行緒池之後,反而會被“反噬”。
比如,你 duang 的一下懟個執行緒池在這裡,我們先只看 web 容器和業務程式碼對應的部分:
由於你的業務程式碼中有執行緒池的存在,所以當接受到一個 web 請求之後,立馬就把請求轉發到了業務執行緒池中,由執行緒池中的執行緒來處理本次請求,從而釋放了 web 請求對應的執行緒,該執行緒又可以裡面去處理其他請求。
這樣來看,你的吞吐量確實上去了。
在前端來看,非常的 nice,請求立馬得到了響應。
但是,你考慮過下游嗎?
你的吞吐量上漲了,下游同一時間處理的請求就變多了。如果下游跟不上處理,頂不住了,直接就是崩給你看怎麼辦?
而且下游不只是你一個呼叫方,由於你呼叫的太猛,導致其他呼叫方的請求響應不過來,是會引起連鎖反應的。
所以,這種場景下,為了非同步懟個執行緒池放著,我覺得還不如用訊息佇列來實現非同步化,頂天了也就是訊息堆積嘛,總比服務崩了好,這樣更加穩妥。
或者至少和下游勾兌一下,問問我們這邊吞吐量上升,你們扛得住不。
有的小夥伴看到這裡可能就會產生一個疑問了:歪師傅,你這個講得怎麼和我背的八股文不一樣啊?
巧了,你背過的八股文我也背過,現在我們來溫習一下我們背過的八股文。
什麼時候使用執行緒池呢?
比如一個請求要經過若干個服務獲取資料,且這些資料沒有先後依賴,最終需要把這些資料組合起來,一併返回,這樣經典的場景:
使用者點商品詳情,你要等半天才展示給使用者,那使用者肯定罵罵咧咧的久走了。
這個時候,八股文上是怎麼說的:用執行緒池來把序列的動作改成並行。
這個場景也是增加了服務 A 的吞吐量,但是用執行緒池就是非常正確的,沒有任何毛病。
但是你想想,我們最開始的這個案例,是這個場景嗎?
我們最開始的案例是想要在業務邏輯中增加一個執行緒池,對著一個下游服務就是一頓猛攻,不是所謂的序列改並行,而是用更多的執行緒,帶來更多的序列。
這已經不是一個概念了。
還有一種場景下,使用執行緒池也是合理的。
比如你有一個定時任務,要從資料庫中撈出狀態為初始化的資料,然後去呼叫另外一個服務的介面查詢資料的最終狀態。
如果你的業務程式碼是這樣的:
//獲取訂單狀態為初始化的資料(0:初始化 1:處理中 2:成功 3:失敗)
//select * from order where order_status=0;
ArrayList initOrderInfoList = queryInitOrderInfoList();
//迴圈處理這批資料
for(OrderInfo orderInfo : initOrderInfoList){
//捕獲異常以免一條資料錯誤導致迴圈結束
try{
//發起rpc呼叫
String orderStatus = queryOrderStatus(orderInfo.getOrderId);
//更新訂單狀態
updateOrderInfo(orderInfo.getOrderId,orderStatus);
} catch (Exception e){
//列印異常
}
}
雖然你框架中使用了執行緒池,但是你就是在一個 for 迴圈中不停的去呼叫下游服務查詢資料狀態,是一條資料一條資料的進行處理,所以其實同一時間,只是使用了框架的執行緒池中的一個執行緒。
為了更加快速的處理完這批資料,這個時候,你就可以懟一個執行緒池放在 for 迴圈裡面了:
//迴圈處理這批資料
for(OrderInfo orderInfo : initOrderInfoList){
//使用執行緒池
executor.execute(() -> {
//捕獲異常以免一條資料錯誤導致迴圈結束
try {
//發起rpc呼叫
String orderStatus = queryOrderStatus(orderInfo.getOrderId);
//更新訂單狀態
updateOrderInfo(orderInfo.getOrderId, orderStatus);
} catch (Exception e) {
//列印異常
}
});
}
需要注意的是,這個執行緒池的引數怎麼去合理的設定,是需要考慮的事情。
同時這個執行緒池的定位,就類似於 web 容器執行緒池的定位。
或者這樣對比起來看更加清晰一點:
定時任務觸發的時候,在發起遠端介面呼叫之前,沒有執行緒池,所以我們可以啟用一個執行緒池來加快資料的處理。
而 Http 呼叫或者 RPC 呼叫,框架中本來就已經有一個執行緒池了,而且也給你提供了對應的效能調優引數配置,那麼首先考慮的應該是把這個執行緒池充分利用起來。
如果僅僅是因為非同步化之後可以提升服務響應速度,沒有達到序列改並行的效果,那麼我更加建議使用訊息佇列。
好了,本文的技術部分就到這裡啦。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2993462/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 多執行緒程式碼示例執行緒
- pytest(13)-多執行緒、多程式執行用例執行緒
- Java 程式中的多執行緒Java執行緒
- Android中的多程式、多執行緒Android執行緒
- 多執行緒,多程式執行緒
- 程式設計思想之多執行緒與多程式(3):Java 中的多執行緒程式設計執行緒Java
- pytest多程式/多執行緒執行測試用例執行緒
- PyQt應用程式中的多執行緒:使用Qt還是Python執行緒?QT執行緒Python
- 多執行緒程式是如何執行程式碼的?執行緒行程
- 程式設計思想之多執行緒與多程式(4):C++ 中的多執行緒程式設計執行緒C++
- 多執行緒------執行緒與程式/執行緒排程/建立執行緒執行緒
- 執行緒以及多執行緒,多程式的選擇執行緒
- 配置監聽器,建立執行緒定時執行業務邏輯執行緒行業
- 多執行緒和多程式模型的選用執行緒模型
- 多執行緒和多程式的區別(小結)執行緒
- 在Web應用程式中執行計劃任務(多執行緒) (轉)Web執行緒
- 多執行緒-多執行緒方式1的程式碼實現執行緒
- 多執行緒-程式和執行緒的概述執行緒
- 多執行緒應用執行緒
- Python——程式、執行緒、協程、多程式、多執行緒(個人向)Python執行緒
- iOS中多執行緒之GCD應用iOS執行緒GC
- C#中的執行緒(三)多執行緒C#執行緒
- Java程式中的多執行緒(1)(轉)Java執行緒
- Java程式中的多執行緒(2)(轉)Java執行緒
- https多執行緒下載程式碼HTTP執行緒
- Java多執行緒1:程式與執行緒概述Java執行緒
- Python 多執行緒多程式Python執行緒
- .NET多執行緒程式設計(1):多工和多執行緒 (轉)執行緒程式設計
- 多執行緒和多執行緒同步執行緒
- 多執行緒-多執行緒方式2的思路及程式碼實現執行緒
- [短文速讀 -5] 多執行緒程式設計引子:程式、執行緒、執行緒安全執行緒程式設計
- python中多執行緒和多程序的應用Python執行緒
- python多執行緒中:如何關閉執行緒?Python執行緒
- Java在不同執行緒中執行程式碼Java執行緒行程
- Java中的多執行緒Java執行緒
- Android《多執行緒-中》Android執行緒
- RxJava 中的多執行緒RxJava執行緒
- Qt 中的多執行緒QT執行緒