讓我們通過本篇文章一同進(jìn)入并發(fā)編程技術(shù)的世界里面,相信通過這篇文文章一定會(huì)對(duì)話你的并發(fā)技術(shù)體系有一定幫助以及夯實(shí)你的基礎(chǔ)功底。
基本概念
- 并發(fā)concurrency
- 并行parallelism
- 吞吐量throughput
并發(fā)操作處理機(jī)制
并發(fā):CPU劃分時(shí)間片,輪流執(zhí)行每個(gè)請(qǐng)求任務(wù),時(shí)間片到期后,換到下一個(gè)

并行操作處理機(jī)制
并行:在多核服務(wù)器上,每個(gè)CPU內(nèi)核執(zhí)行一個(gè)任務(wù),是真正的并行

吞吐量
單位時(shí)間內(nèi)服務(wù)器總的請(qǐng)求處理量
- 以 request/second 來衡量,如1200rps
- 每個(gè)請(qǐng)求的處理時(shí)間latency
- 服務(wù)器處理請(qǐng)求的并發(fā)workers
- 其他因素如GC也會(huì)影響吞吐量
CSDN new bbs 的案例
- 平均每個(gè)請(qǐng)求的latency – 200ms
- 總共40個(gè)workers
- 理論吞吐量上限 1000/200*40 = 200rps
- 理論每日處理動(dòng)態(tài)請(qǐng)求上限1700萬,目前實(shí)際每日處理動(dòng)態(tài)請(qǐng)求270-330萬,預(yù)估實(shí)際處理上限600萬
IO類型
- 磁盤文件操作,例如讀硬盤文件
- 操作系統(tǒng)調(diào)用,例如shell命令
- 網(wǎng)絡(luò)操作
- 訪問數(shù)據(jù)庫 MySQL, MongoDB, ...
- 訪問其他Web服務(wù),發(fā)起網(wǎng)絡(luò)連接
- 訪問緩存服務(wù)器 Memcached, Redis
IO密集請(qǐng)求
IO操作的延時(shí)遠(yuǎn)遠(yuǎn)高于CPU時(shí)鐘周期和內(nèi)存訪問,所以一旦Web請(qǐng)求涉及IO操作,CPU處于wait狀態(tài),被浪費(fèi)了。

IO密集型并發(fā)
并發(fā)真能提高吞吐量嗎?
假設(shè)每個(gè)請(qǐng)求執(zhí)行100ms,順序執(zhí)行10個(gè)請(qǐng)求共需要1s
單核服務(wù)器并發(fā)處理10個(gè)請(qǐng)求,假設(shè)平均分配時(shí)間片10ms,請(qǐng)求1到請(qǐng)求10將在900ms到1000ms間執(zhí)行完畢。
順序執(zhí)行10個(gè)請(qǐng)求,每個(gè)請(qǐng)求100ms,總共1s執(zhí)行完畢

并發(fā)執(zhí)行10個(gè)請(qǐng)求,每個(gè)請(qǐng)求分配10ms的時(shí)間片,仍然1s執(zhí)行完畢
吞吐量沒有提高,每個(gè)請(qǐng)求處理時(shí)間變長。
吞吐量沒有任何提高。并發(fā)越多,所有請(qǐng)求都變得非常緩慢。(考慮到任務(wù)的場(chǎng)景切換開銷,吞吐量還會(huì)下降,需要超過1s才能執(zhí)行完畢)。
大多數(shù)Web型應(yīng)用都是IO密集型
并發(fā)執(zhí)行10個(gè)請(qǐng)求,每個(gè)請(qǐng)求分配10ms的時(shí)間片
200ms之后CPU處于空閑狀態(tài)
執(zhí)行請(qǐng)求100ms當(dāng)中,可能有80ms花在IO上,只有20ms消耗CPU時(shí)鐘周期,最好情況下,請(qǐng)求1到請(qǐng)求10將在190ms到280ms間執(zhí)行完畢,吞吐量極大提高。

- IO密集型應(yīng)用,大部分CPU花在等待IO上了,所以并發(fā)可以有效提高系統(tǒng)的吞吐量
并發(fā)和并行
純CPU密集型的應(yīng)用
- 在單核上并發(fā)執(zhí)行多個(gè)請(qǐng)求,不能提高吞吐量
- 由于任務(wù)來回場(chǎng)景切換的開銷,吞吐量反而會(huì)下降
- 只有多核并行運(yùn)算,才能有效提高吞吐量
IO密集型的應(yīng)用
由于請(qǐng)求過程中,很多時(shí)間都是外部IO操作,CPU在wait狀態(tài),所以并發(fā)執(zhí)行可以有效提高系統(tǒng)吞吐量。
并發(fā)模型模型發(fā)展
- multi-process(多進(jìn)程)
- multi-thread(多線程)
- multi-process + multi-thread(GIL)(多進(jìn)程+多線程)
- event I/O(事件驅(qū)動(dòng))
- coroutine(協(xié)程)
常見多進(jìn)程Web服務(wù)端編程模型
- PHP
- Python
- Ruby
多進(jìn)程優(yōu)點(diǎn)
- 并發(fā)模型非常簡(jiǎn)單
- 由操作系統(tǒng)調(diào)度運(yùn)行穩(wěn)定強(qiáng)壯
- 非常容易管理
- 很容易通過操作系統(tǒng)方便的監(jiān)控,例如每個(gè)進(jìn)程CPU,內(nèi)存變化狀況,甚至可以觀測(cè)到進(jìn)程處理什么Web請(qǐng)求很容易通過操作系統(tǒng)管理進(jìn)程,例如可以通過給進(jìn)程發(fā)送signal,實(shí)現(xiàn)各種管理: unicorn。
- 隔離性非常好
- 一個(gè)進(jìn)程崩潰不會(huì)影響其他進(jìn)程
- 某進(jìn)程出現(xiàn)問題的時(shí)候,只要?dú)⒌羲貑⒓纯?,不影響整體服務(wù)的可用性
- 很容易實(shí)現(xiàn)在線熱部署和無縫升級(jí)
- 代碼兼容性極好,不必考慮線程安全問題
- 多進(jìn)程可以有效利用多核CPU,實(shí)現(xiàn)并行處理
多進(jìn)程監(jiān)控
- 監(jiān)控進(jìn)程CPU top –p pid
- 簡(jiǎn)單處理甚至可以查看進(jìn)程處理的URL請(qǐng)求
- 監(jiān)控進(jìn)程的IO iotop –p pid
- 監(jiān)控進(jìn)程的物理內(nèi)存使用 ps, /proc
多進(jìn)程缺點(diǎn)
內(nèi)存消耗很多
每個(gè)獨(dú)立進(jìn)程都需要加載完整的應(yīng)用環(huán)境,內(nèi)存消耗超大。(COW模式可以緩解這個(gè)問題)
例如每個(gè)Rails進(jìn)程物理內(nèi)存占用為150MB,20個(gè)workers,則需要3GB物理內(nèi)存。
CPU消耗偏高
多進(jìn)程并發(fā),需要CPU內(nèi)核在多個(gè)進(jìn)程間頻繁切換,而進(jìn)程的場(chǎng)景切換(context switch)是非常昂貴的,需要大量的內(nèi)存換頁操作。
很低的I/O并發(fā)處理能力
-
多進(jìn)程的并發(fā)能力非常有限
- 每個(gè)進(jìn)程只能并發(fā)處理1個(gè)請(qǐng)求
- 單臺(tái)服務(wù)器啟動(dòng)的進(jìn)程數(shù)有限,并發(fā)處理能力無法有效提高
-
只適合處理短請(qǐng)求,不適合處理長請(qǐng)求
- 每個(gè)請(qǐng)求都能在很短時(shí)間內(nèi)執(zhí)行完畢,因而不會(huì)造成進(jìn)程被長期阻塞一旦某個(gè)操作特別是IO操作阻塞,就會(huì)造成進(jìn)程阻塞
- 當(dāng)大面積IO操作阻塞發(fā)生,服務(wù)器就無法響應(yīng)了
- 對(duì)于無法預(yù)知的外部IO操作,應(yīng)用代碼必須設(shè)置timeout參數(shù),以防進(jìn)程阻塞
緩解多進(jìn)程低IO并發(fā)問題
-
用nginx做前端Web Server
適當(dāng)增大proxy buffer size,避免多進(jìn)程request/response buffer IO開銷
使用X-sendfile,避免多進(jìn)程讀取大文件IO開銷
-
凡IO操作都要設(shè)置timeout
- 避免無法預(yù)知的IO掛起造成進(jìn)程阻塞
長請(qǐng)求和短請(qǐng)求分離開,不要放在一起
multi-thread多線程操作模型
常見多線程模型(1:1)
1 native thread : 1 process thread
在一個(gè)重量級(jí)進(jìn)程當(dāng)中啟動(dòng)多個(gè)線程并發(fā)處理請(qǐng)求
-
多線程并發(fā)
- 每個(gè)線程可以并發(fā)處理1個(gè)請(qǐng)求,并發(fā)能力取決于線程數(shù)量線程的調(diào)度由VM負(fù)責(zé),可以通過編程控制
多線程優(yōu)點(diǎn)
多線程并發(fā)內(nèi)存消耗比較少
- 每個(gè)線程需要一個(gè)thread stack保存線程場(chǎng)景,thread stack一般只需要十幾到幾十KB內(nèi)存,不像多進(jìn)程,每個(gè)進(jìn)程需要加載完整的應(yīng)用環(huán)境,需要分配十幾到上百M(fèi)B內(nèi)存。
- 線程可以共享資源,特別是可以共享整個(gè)應(yīng)用環(huán)境,不必像多進(jìn)程每個(gè)進(jìn)程要加載應(yīng)用環(huán)境。
多線程并發(fā)CPU消耗比較小
- 線程的場(chǎng)景切換開銷小于進(jìn)程的場(chǎng)景切換
很容易創(chuàng)建和高效利用共享資源
- 數(shù)據(jù)庫線程池
- 字典表,進(jìn)程內(nèi)緩存......
IO并發(fā)能力很高
- Java VM可以輕松維護(hù)幾百個(gè)并發(fā)線程的線程切換開銷,遠(yuǎn)高于多進(jìn)程單服務(wù)器上幾十個(gè)并發(fā)的處理能力
可有效利用多核CPU,實(shí)現(xiàn)并行運(yùn)算
多線程的缺點(diǎn)
VM的內(nèi)存管理要求超高
- 對(duì)內(nèi)存管理要求非常高,應(yīng)用代碼稍不注意,就會(huì)產(chǎn)生OOM(out of memory),需要應(yīng)用代碼長期和內(nèi)存泄露做斗爭(zhēng)
- GC的策略會(huì)影響多線程并發(fā)能力和系統(tǒng)吞吐量,需要對(duì)GC策略和調(diào)優(yōu)有很好的經(jīng)驗(yàn)
- 在大內(nèi)存服務(wù)器上的物理內(nèi)存利用率問題
對(duì)共享資源的操作
- 對(duì)共享資源的操作要非常小心,特別是修改共享資源需要加鎖操作,很容易引發(fā)死鎖問題
應(yīng)用代碼和第三方庫都必須是線程安全的
- 使用了非線程安全的庫會(huì)造成各種潛在難以排查的問題
單進(jìn)程多線程模型不方便通過操作系統(tǒng)管理
- 一旦出現(xiàn)線程死鎖或者線程阻塞很容易導(dǎo)致整個(gè)VM進(jìn)程掛起失去響應(yīng),隔離性很差
multi-thread with GIL
- Global Interpeter Lock:有限制的并發(fā)
- IO操作或者操作系統(tǒng)調(diào)用,釋放鎖,多線程IO并發(fā)
- 由于加鎖,無法利用多核,只能使用1個(gè)CPU內(nèi)核,因而無法實(shí)現(xiàn)多核并行運(yùn)算
提供簡(jiǎn)化的并發(fā)策略
對(duì)CPU密集型運(yùn)算,并發(fā)不能提高吞吐量:加鎖,禁止并發(fā)
對(duì)IO密集型運(yùn)算,并發(fā)可以有效提高吞吐量:解鎖,允許多線程并發(fā)
性能
對(duì)CPU密集型運(yùn)算,多線程并發(fā)由于線程場(chǎng)景切換帶來的開銷,吞吐量要差于單進(jìn)程順序執(zhí)行
兼容性
加鎖可以保證代碼和庫的兼容性

multi-process + multi-thread(GIL)
- 由于GIL,多線程只能跑在1個(gè)CPU內(nèi)核上,無法有效利用多核CPU,跑多個(gè)進(jìn)程可以有效利用多核,一般進(jìn)程數(shù)略多于服務(wù)器CPU內(nèi)核數(shù)
- 一個(gè)進(jìn)程不宜跑過多線程,否則會(huì)引發(fā)嚴(yán)重的GC內(nèi)存管理問題
pros and cons
- 內(nèi)存消耗低于單純的多進(jìn)程并發(fā)
- 非常有效的提高了IO并發(fā)處理能力
- IO庫和操作系統(tǒng)調(diào)用庫必須保證線程安全
event IO
常見event IO編程模型
- Nginx / Lighttpd
- Ruby EventMachine / Python Twisted
- node.js
event IO原理
- 單進(jìn)程單線程
- 內(nèi)部維護(hù)一個(gè)事件隊(duì)列
- 每個(gè)請(qǐng)求切成多個(gè)事件
- 每個(gè)IO調(diào)用切成一個(gè)事件
- 編程調(diào)用process.next_Tick()方法切分事件
- 單進(jìn)程順序從事件隊(duì)列當(dāng)中取出每個(gè)事件執(zhí)行下去

event IO的優(yōu)點(diǎn)
驚人的IO并發(fā)處理能力
- nginx單機(jī)可以處理50K以上的HTTP并發(fā)連接
- node.js單機(jī)可以處理幾千上萬個(gè)HTTP并發(fā)連接
極少的內(nèi)存消耗
單一進(jìn)程單一線程,無場(chǎng)景切換無需保存場(chǎng)景
CPU消耗偏低
無進(jìn)程或者線程場(chǎng)景切換的開銷
event IO的缺點(diǎn)
必須使用異步編程
異步編程是一種原始的編程方式
代碼量和復(fù)雜度都會(huì)有很大的增加,提高了編程的難度,以及開發(fā)和維護(hù)成本
復(fù)雜的業(yè)務(wù)邏輯(例如工作流業(yè)務(wù))會(huì)造成代碼迅速膨脹,極難維護(hù)
異步事件流使得異常處理和調(diào)試有很大困難
CPU密集型的運(yùn)算會(huì)阻塞住整個(gè)進(jìn)程
需要通過編程,將密集型的任務(wù)拆分為多個(gè)事件
所有IO操作必須使用異步庫
一旦不小心使用同步IO操作,會(huì)造成整個(gè)進(jìn)程阻塞,庫的兼容性必須非常小心
只能跑在1個(gè)CPU內(nèi)核上,無法有效利用多核并行運(yùn)算
運(yùn)行多個(gè)進(jìn)程來利用多核CPU
coroutine原理
- 在單個(gè)線程上運(yùn)行多個(gè)纖程,每個(gè)纖程維護(hù)1個(gè)context
- 纖程非常輕量級(jí),單個(gè)線程可以輕易維護(hù)幾萬個(gè)纖程
- 纖程調(diào)度依賴于應(yīng)用程序框架
- 纖程切換
- 必須自己編程來實(shí)現(xiàn)
- 一般應(yīng)用層代碼不需要編程,框架層實(shí)現(xiàn)纖程調(diào)度
- 纖程本質(zhì)上是基于event IO之上的高級(jí)封裝,但消除了event IO原始的異步編程復(fù)雜度

單一線程通過程序調(diào)度了3個(gè)纖程并發(fā),底層仍然是event IO驅(qū)動(dòng)
但是有3個(gè)清晰的并發(fā)執(zhí)行體,仍然是同步并發(fā)編程風(fēng)格,但實(shí)現(xiàn)了異步驅(qū)動(dòng)
coroutine的優(yōu)點(diǎn)
- 支持極高的IO并發(fā),和event IO基本相當(dāng)
- 纖程的創(chuàng)建和切換的系統(tǒng)開銷非常小,CPU和內(nèi)存消耗都很小
- 編程方式和常見的同步編程基本一致,是event IO的高級(jí)封裝形式
coroutine的缺點(diǎn)
- 纖程運(yùn)行在單線程上,無法有效利用多核實(shí)現(xiàn)并行運(yùn)算
- 通過啟動(dòng)多個(gè)進(jìn)程或者多個(gè)線程來利用多核CPU
- CPU密集型的運(yùn)算會(huì)阻塞住整個(gè)進(jìn)程
- 通過編程,將密集型的任務(wù)拆分為多步
- 所有IO操作必須使用異步庫
- 一旦不小心使用同步IO操作,會(huì)造成整個(gè)進(jìn)程阻塞,庫的兼容性必須非常小心