在講協(xié)程前, 我們先點名幾個概念
并行和并發(fā)
摘自知乎的一個例子:
- 你正在吃飯, 當(dāng)你電話來了, 你一直到吃完了才去接電話 =》 不支持并發(fā)和并行
- 你正在吃飯, 當(dāng)你電話來了, 你放下筷子, 去接電話, 打完后繼續(xù)吃飯 =》支持并發(fā)
- 你正在吃飯, 當(dāng)你電話來了, 你一邊接電話, 一邊吃飯 =》 支持并行
并發(fā)和并行都有處理多任務(wù)的能力(吃飯和接電話), 但兩者的區(qū)別在于是否能同時
- 并發(fā)的關(guān)鍵是你能處理多任務(wù), 但不是同時
- 并行的關(guān)鍵是你能同時處理多任務(wù), 任務(wù)的最大上限取決于CPU的核數(shù)
用戶態(tài)和內(nèi)核態(tài)
還記得進程結(jié)構(gòu)嗎?
一個進程實例地址被分成了兩部分, 內(nèi)核空間 和 用戶空間. 在32位的內(nèi)存中, 每個進程最高的1G空間稱自為內(nèi)核空間, 供所有進程共享. 而剩下的3G稱自為用戶空間, 供自己使用
CPU指令集也一樣, CPU的所有指令中, 有些指令是非常危險的, 如果錯用, 將導(dǎo)致系統(tǒng)崩潰(比如,清理內(nèi)存, 設(shè)置時鐘等). 所以我們將指令分成兩個等級: 特權(quán)指令 和 非特權(quán)指令.
無論是內(nèi)核空間還是特權(quán)指令, 都統(tǒng)一交給操作系統(tǒng)的核心 內(nèi)核 來負責(zé), 它獨立于普通應(yīng)用,可以訪問被保護的內(nèi)存空間地址(內(nèi)核地址), 也有訪問底層硬件的權(quán)限.
內(nèi)核態(tài)是提供給內(nèi)核的運行空間, 而用戶態(tài)是提供給應(yīng)用程序運行的空間, 兩者所能使用的指令和內(nèi)存都是不一樣的, 這樣有效地保護了操作系統(tǒng)的安全, 當(dāng)程序崩潰時, 不會影響到操作系統(tǒng)
這里你就會納悶, 好端端的運行程序, 為什么會進入到內(nèi)核態(tài)?
用戶態(tài)進入內(nèi)核態(tài)的三種方式:
- 系統(tǒng)調(diào)用(System Call)
- 缺頁中斷(相應(yīng)的虛擬地址沒有映射到物理地址)
- 中斷(網(wǎng)絡(luò), I/O)
系統(tǒng)調(diào)用相當(dāng)于用戶態(tài)和內(nèi)核態(tài)的中介, 為了使用戶態(tài)的程序能夠訪問到內(nèi)核管理的資源(CPU, 磁盤等), 內(nèi)核提供的一組通用訪問接口, 這些接口就叫做 系統(tǒng)調(diào)用
當(dāng)用戶態(tài)進入內(nèi)核態(tài) 或者 內(nèi)核態(tài)進入用戶態(tài) 都需要進行上下文(CPU上下文)切換的, 所以一次系統(tǒng)調(diào)用會產(chǎn)生兩次 CPU上下文切換.
協(xié)程
協(xié)程是什么?
協(xié)程, 英文Coroutines, 是一種比線程更輕量級的存在. 正如一個進程可以有多個線程, 一個線程也可以有多個協(xié)程. 它的特性體現(xiàn)在 它不是由操作系統(tǒng)內(nèi)核來管理, 而完全由程序所控制
這點引用特性出了它的核心, 程序所控制. 這會導(dǎo)致在進行協(xié)程切換的時候, 我們不需要從用戶態(tài)到內(nèi)核態(tài), 這無形中就減少了CPU上下文切換所帶來的消耗
盡管線程是輕量級的進程, 它共享著進程的資源, 但它的切換是由內(nèi)核來管理的, 這就意味著每次切換都需要從用戶態(tài)(線程A)到內(nèi)核態(tài), 再從內(nèi)核態(tài)到用戶態(tài)(線程B), 避免不了CPU帶來的消耗
協(xié)程是早年間就有的東西, 但直到現(xiàn)在才開始流行, 這其中的原由離不開 - 高并發(fā)
高并發(fā)下的協(xié)程
隨著互聯(lián)網(wǎng)流量的不斷增大, 請求從百萬到千萬甚至更多. 高并發(fā)下的技術(shù)也一直在演變. 從早期的 多進程 -> 多線程 -> NIO(多路復(fù)用, 異步回調(diào)) -> 協(xié)程
本段參考 05 | 協(xié)程:如何快速地實現(xiàn)高并發(fā)服務(wù) 和 當(dāng)我們在討論高并發(fā)的時候, 在討論著什么, 侵權(quán)則刪
無論是多線程還是多進程, 當(dāng)我們發(fā)起I/O請求時, 會導(dǎo)致線程的阻塞, 等待請求完成. 這個時候內(nèi)核就會進行線程切換, 等到 I/O 響應(yīng)時, 再切換回來
一說到切換, 開銷就少不了. 特別是面對千萬請求時, 這樣的開銷是機器無法承受的. 人們提出了新的想法, 即如何避免切換?, 或者說如何避免阻塞
陶老師提到很關(guān)鍵的一點, 把內(nèi)核實現(xiàn)切換的請求工作, 轉(zhuǎn)換到用戶態(tài)來完成.
異步化編程通過一個叫做 Dispatcher 的單線程主循環(huán)(又叫 event loop), 用戶向 Dispatcher 注冊回調(diào)函數(shù), 來實現(xiàn)異步通知, 從而不必在原地干等消費資源. 整個過程由單線程來實現(xiàn), 在Dispatcher主循環(huán)中通過 select()/ epoll()
等系統(tǒng)調(diào)用來等待各種I/O事件的發(fā)生, 并調(diào)用相應(yīng)的回調(diào)函數(shù)(event handle)來處理請求
通過將阻塞方法改造程了非阻塞方法, 和單線程的使用, 將并發(fā)提升到了百萬級請求級別. 但是異步化代碼容易出錯, 而且callback的回調(diào)地獄也讓人頭疼, 阻塞函數(shù)都需要經(jīng)過非阻塞系統(tǒng)拆分成了兩部分, 第一個函數(shù)由你發(fā)起, 第二個函數(shù)由多路復(fù)用機制調(diào)用, 導(dǎo)致代碼太過于復(fù)雜和晦澀
這時候, 人們把目光指向了協(xié)程
A coroutine is a function that can suspend its execution (yield) until the given YieldInstruction finishes.
協(xié)程和異步化編程一樣, 都是通過用戶態(tài)來調(diào)度. 官方的定義中把 協(xié)程比做了函數(shù), 這個函數(shù)在執(zhí)行時, 如果遇到了阻塞, 會使執(zhí)行它的協(xié)程無感知的自動放棄執(zhí)行權(quán), 由協(xié)程框架切換到其他就緒的協(xié)程繼續(xù)執(zhí)行, 當(dāng)結(jié)果滿足后, 該框架選擇合適的時機后切換回它所在的協(xié)程繼續(xù)執(zhí)行
這里又引出了協(xié)程的另一大好處, 即用同步編程的方式來寫高并發(fā)程序
協(xié)程的切換類似于內(nèi)核的切換, 保存兩個最重要的寄存器值(指令寄存器, 棧寄存器). 協(xié)程都有自己的棧, 是從進程的堆中分配的, 很小很小一塊. 這也是為什么說, 一個線程能運行百萬個協(xié)程
協(xié)程的優(yōu)缺點
協(xié)程的優(yōu)點: 我們在總結(jié)下
- 協(xié)程更輕量, 內(nèi)存消耗下, 創(chuàng)建成本小
- 協(xié)程大大減少了上下文切換的消耗
- 減少同步加鎖, 這個是很關(guān)鍵的一點. 因為協(xié)程是協(xié)作式任務(wù)分配, 就自己主動放棄, 讓別人接著運行. 而線程是搶占式的
- 按照同步的思維來寫異步代碼
缺點:
- 在協(xié)程中不能有阻塞操作, 否則會導(dǎo)致整個線程阻塞(因為要自己主動放棄, 如果阻塞了, 怎么放棄)
- 協(xié)程可以處理I/O密集型應(yīng)用, 對CPU密集型不得行(因為CPU密集型一直在算, 哪有時間切換給其他協(xié)程)
總結(jié)
協(xié)程到了就差不大了, 其實主要是理解它的思想和應(yīng)用. 之所以會不斷運用新技術(shù), 也是因為為了更好的壓榨CPU的有效使用
最后, 我們來看看 協(xié)程 和 線程的關(guān)系
這兩者其實并不是互相排斥的, 在很多場景, 都會有 協(xié)程 + 線程的結(jié)合. 比如當(dāng)我們遇到無法將阻塞改成非阻塞狀態(tài)的函數(shù)時, 我們需要線程的調(diào)用. 其次, 無論開多少個協(xié)程, 本質(zhì)上使用的是一個線程, 那用的就是單核CPU, 我們需要借助多線程來使用多核CPU
如果有什么疑問和錯誤, 歡迎指出, 感謝你的支持