讓我們通過本篇文章一同進入并發編程技術的世界里面,相信通過這篇文文章一定會對話你的并發技術體系有一定幫助以及夯實你的基礎功底。
【資料圖】
基本概念
并發concurrency并行parallelism吞吐量throughput
并發操作處理機制
并發:CPU劃分時間片,輪流執行每個請求任務,時間片到期后,換到下一個
并行操作處理機制
并行:在多核服務器上,每個CPU內核執行一個任務,是真正的并行
吞吐量
單位時間內服務器總的請求處理量
以 request/second 來衡量,如1200rps每個請求的處理時間latency服務器處理請求的并發workers其他因素如GC也會影響吞吐量
CSDN new bbs 的案例
平均每個請求的latency – 200ms總共40個workers理論吞吐量上限 1000/200*40 = 200rps理論每日處理動態請求上限1700萬,目前實際每日處理動態請求270-330萬,預估實際處理上限600萬
IO類型
磁盤文件操作,例如讀硬盤文件操作系統調用,例如shell命令網絡操作訪問數據庫 MySQL, MongoDB, ...訪問其他Web服務,發起網絡連接訪問緩存服務器 Memcached, Redis
IO密集請求
IO操作的延時遠遠高于CPU時鐘周期和內存訪問,所以一旦Web請求涉及IO操作,CPU處于wait狀態,被浪費了。
IO密集型并發
并發真能提高吞吐量嗎?
假設每個請求執行100ms,順序執行10個請求共需要1s單核服務器并發處理10個請求,假設平均分配時間片10ms,請求1到請求10將在900ms到1000ms間執行完畢。
順序執行10個請求,每個請求100ms,總共1s執行完畢
并發執行10個請求,每個請求分配10ms的時間片,仍然1s執行完畢吞吐量沒有提高,每個請求處理時間變長。
吞吐量沒有任何提高。并發越多,所有請求都變得非常緩慢。(考慮到任務的場景切換開銷,吞吐量還會下降,需要超過1s才能執行完畢)。
大多數Web型應用都是IO密集型
并發執行10個請求,每個請求分配10ms的時間片200ms之后CPU處于空閑狀態執行請求100ms當中,可能有80ms花在IO上,只有20ms消耗CPU時鐘周期,最好情況下,請求1到請求10將在190ms到280ms間執行完畢,吞吐量極大提高。IO密集型應用,大部分CPU花在等待IO上了,所以并發可以有效提高系統的吞吐量
并發和并行
純CPU密集型的應用
在單核上并發執行多個請求,不能提高吞吐量由于任務來回場景切換的開銷,吞吐量反而會下降只有多核并行運算,才能有效提高吞吐量
IO密集型的應用
由于請求過程中,很多時間都是外部IO操作,CPU在wait狀態,所以并發執行可以有效提高系統吞吐量。
并發模型模型發展
multi-process(多進程)multi-thread(多線程)multi-process + multi-thread(GIL)(多進程+多線程)event I/O(事件驅動)coroutine(協程)
常見多進程Web服務端編程模型
PHPPythonRuby
多進程優點
并發模型非常簡單由操作系統調度運行穩定強壯非常容易管理很容易通過操作系統方便的監控,例如每個進程CPU,內存變化狀況,甚至可以觀測到進程處理什么Web請求很容易通過操作系統管理進程,例如可以通過給進程發送signal,實現各種管理: unicorn。隔離性非常好一個進程崩潰不會影響其他進程某進程出現問題的時候,只要殺掉它重啟即可,不影響整體服務的可用性很容易實現在線熱部署和無縫升級代碼兼容性極好,不必考慮線程安全問題多進程可以有效利用多核CPU,實現并行處理
多進程監控
監控進程CPU top –p pid簡單處理甚至可以查看進程處理的URL請求監控進程的IO iotop –p pid監控進程的物理內存使用 ps, /proc
多進程缺點
內存消耗很多
每個獨立進程都需要加載完整的應用環境,內存消耗超大。(COW模式可以緩解這個問題)
例如每個Rails進程物理內存占用為150MB,20個workers,則需要3GB物理內存。
CPU消耗偏高
多進程并發,需要CPU內核在多個進程間頻繁切換,而進程的場景切換(context switch)是非常昂貴的,需要大量的內存換頁操作。
很低的I/O并發處理能力
多進程的并發能力非常有限每個進程只能并發處理1個請求單臺服務器啟動的進程數有限,并發處理能力無法有效提高只適合處理短請求,不適合處理長請求每個請求都能在很短時間內執行完畢,因而不會造成進程被長期阻塞一旦某個操作特別是IO操作阻塞,就會造成進程阻塞當大面積IO操作阻塞發生,服務器就無法響應了對于無法預知的外部IO操作,應用代碼必須設置timeout參數,以防進程阻塞
緩解多進程低IO并發問題
用nginx做前端Web Server適當增大proxy buffer size,避免多進程request/response buffer IO開銷使用X-sendfile,避免多進程讀取大文件IO開銷凡IO操作都要設置timeout避免無法預知的IO掛起造成進程阻塞長請求和短請求分離開,不要放在一起
multi-thread多線程操作模型
常見多線程模型(1:1)
1 native thread : 1 process thread
在一個重量級進程當中啟動多個線程并發處理請求多線程并發每個線程可以并發處理1個請求,并發能力取決于線程數量線程的調度由VM負責,可以通過編程控制
多線程優點
多線程并發內存消耗比較少
每個線程需要一個thread stack保存線程場景,thread stack一般只需要十幾到幾十KB內存,不像多進程,每個進程需要加載完整的應用環境,需要分配十幾到上百MB內存。線程可以共享資源,特別是可以共享整個應用環境,不必像多進程每個進程要加載應用環境。
多線程并發CPU消耗比較小
線程的場景切換開銷小于進程的場景切換
很容易創建和高效利用共享資源
數據庫線程池字典表,進程內緩存......
IO并發能力很高
Java VM可以輕松維護幾百個并發線程的線程切換開銷,遠高于多進程單服務器上幾十個并發的處理能力
可有效利用多核CPU,實現并行運算
多線程的缺點
VM的內存管理要求超高
對內存管理要求非常高,應用代碼稍不注意,就會產生OOM(out of memory),需要應用代碼長期和內存泄露做斗爭GC的策略會影響多線程并發能力和系統吞吐量,需要對GC策略和調優有很好的經驗在大內存服務器上的物理內存利用率問題
對共享資源的操作
對共享資源的操作要非常小心,特別是修改共享資源需要加鎖操作,很容易引發死鎖問題
應用代碼和第三方庫都必須是線程安全的
使用了非線程安全的庫會造成各種潛在難以排查的問題
單進程多線程模型不方便通過操作系統管理
一旦出現線程死鎖或者線程阻塞很容易導致整個VM進程掛起失去響應,隔離性很差
multi-thread with GIL
Global Interpeter Lock:有限制的并發IO操作或者操作系統調用,釋放鎖,多線程IO并發由于加鎖,無法利用多核,只能使用1個CPU內核,因而無法實現多核并行運算
提供簡化的并發策略
對CPU密集型運算,并發不能提高吞吐量:加鎖,禁止并發對IO密集型運算,并發可以有效提高吞吐量:解鎖,允許多線程并發
性能
對CPU密集型運算,多線程并發由于線程場景切換帶來的開銷,吞吐量要差于單進程順序執行
兼容性
加鎖可以保證代碼和庫的兼容性
multi-process + multi-thread(GIL)
由于GIL,多線程只能跑在1個CPU內核上,無法有效利用多核CPU,跑多個進程可以有效利用多核,一般進程數略多于服務器CPU內核數一個進程不宜跑過多線程,否則會引發嚴重的GC內存管理問題
pros and cons
內存消耗低于單純的多進程并發非常有效的提高了IO并發處理能力IO庫和操作系統調用庫必須保證線程安全
event IO
常見event IO編程模型
Nginx / LighttpdRuby EventMachine / Python Twistednode.js
event IO原理
單進程單線程內部維護一個事件隊列每個請求切成多個事件每個IO調用切成一個事件編程調用process.next_Tick()方法切分事件單進程順序從事件隊列當中取出每個事件執行下去
event IO的優點
驚人的IO并發處理能力
nginx單機可以處理50K以上的HTTP并發連接node.js單機可以處理幾千上萬個HTTP并發連接
極少的內存消耗
單一進程單一線程,無場景切換無需保存場景
CPU消耗偏低
無進程或者線程場景切換的開銷
event IO的缺點
必須使用異步編程
異步編程是一種原始的編程方式代碼量和復雜度都會有很大的增加,提高了編程的難度,以及開發和維護成本復雜的業務邏輯(例如工作流業務)會造成代碼迅速膨脹,極難維護異步事件流使得異常處理和調試有很大困難
CPU密集型的運算會阻塞住整個進程
需要通過編程,將密集型的任務拆分為多個事件
所有IO操作必須使用異步庫
一旦不小心使用同步IO操作,會造成整個進程阻塞,庫的兼容性必須非常小心
只能跑在1個CPU內核上,無法有效利用多核并行運算
運行多個進程來利用多核CPU
coroutine原理
在單個線程上運行多個纖程,每個纖程維護1個context纖程非常輕量級,單個線程可以輕易維護幾萬個纖程纖程調度依賴于應用程序框架纖程切換必須自己編程來實現一般應用層代碼不需要編程,框架層實現纖程調度纖程本質上是基于event IO之上的高級封裝,但消除了event IO原始的異步編程復雜度
單一線程通過程序調度了3個纖程并發,底層仍然是event IO驅動但是有3個清晰的并發執行體,仍然是同步并發編程風格,但實現了異步驅動
coroutine的優點
支持極高的IO并發,和event IO基本相當纖程的創建和切換的系統開銷非常小,CPU和內存消耗都很小編程方式和常見的同步編程基本一致,是event IO的高級封裝形式
coroutine的缺點
纖程運行在單線程上,無法有效利用多核實現并行運算通過啟動多個進程或者多個線程來利用多核CPUCPU密集型的運算會阻塞住整個進程通過編程,將密集型的任務拆分為多步所有IO操作必須使用異步庫一旦不小心使用同步IO操作,會造成整個進程阻塞,庫的兼容性必須非常小心
參考資料
??multithreaded-rails-is-generally-better????threads-in-ruby-enough-already????about-concurrency-and-the-gil????the-ruby-global-interpreter-lock??