協程概念
“協程”(Coroutine)概念最早由 Melvin Conway 於 1958 年提出。雖然被提出的時間很早,但是使用它的年限很短。尤其是最近幾年,隨着 Go、Lua 等語言的流行,把協程推向了一個新的高潮。
協程其實可以認為是比線程更小的執行單元。為啥說他是一個執行單元,因為他自帶CPU上下文。這樣只要在合適的時機,我們可以把一個協程 切換到 另一個協程。只要這個過程中保存或恢復 CPU上下文那么程序還是可以運行的。
協程可以理解為純用戶態的線程,其通過協作而不是搶占來進行切換。相對於進程或者線程,協程所有的操作都可以在用戶態完成,創建和切換的消耗更低。總的來說,協程為協同任務提供了一種運行時抽象,這種抽象非常適合於協同多任務調度和數據流處理。在現代操作系統和編程語言中,因為用戶態線程切換代價比內核態線程小,協程成為了一種輕量級的多任務模型。
從編程角度上看,協程的思想本質上就是控制流的主動讓出(yield)和恢復(resume)機制,迭代器常被用來實現協程,所以大部分的語言實現的協程中都有 yield 關鍵字,比如 Python、Lua。但也有特殊比如 Go 就使用的是通道來通信。先來張圖看看:
協程和線程差異
那么這個過程看起來比線程差不多哇。其實不然 線程切換從系統層面遠不止 保存和恢復 CPU上下文這么簡單。操作系統為了程序運行的高效性每個線程都有自己緩存Cache等等數據,操作系統還會幫你做這些數據的恢復操作。所以線程的切換非常耗性能。但是協程的切換只是單純的操作CPU的上下文,所以一秒鍾切換個上百萬次系統都抗的住。
協程的問題
但是協程有一個問題,就是系統並不感知,所以操作系統不會幫你做切換。那么誰來幫你做切換?讓需要執行的協程更多的獲得CPU時間才是問題的關鍵。
協程目前主流設計
目前的協程框架一般都是設計成 1:N 模式。所謂 1:N 就是一個線程作為一個容器里面放置多個協程。那么誰來適時的切換這些協程?答案是有協程自己主動讓出CPU,也就是每個協程池里面有一個調度器,這個調度器是被動調度的。意思就是他不會主動調度。而且當一個協程發現自己執行不下去了(比如異步等待網絡的數據回來,但是當前還沒有數據到),這個時候就可以由這個協程通知調度器,這個時候執行到調度器的代碼,調度器根據事先設計好的調度算法找到當前最需要CPU的協程。切換這個協程的CPU上下文把CPU的運行權交個這個協程,直到這個協程出現執行不下去需要等等的情況,或者它調用主動讓出CPU的API之類,觸發下一次調度。對的沒錯就是類似於 領導人模式
這個設計有問題嗎?
其實是有問題的,假設這個線程中有一個協程是CPU密集型的且沒有IO操作,也就是自己不會主動觸發調度器調度的過程,那么就會出現其他協程得不到執行的情況,所以這種情況下需要程序員自己避免。這是一個問題,假設業務開發的人員並不懂這個原理的話就可能會出現問題。
協程舉例理解
在所有語言中都存在着層級調用,比如 A 調用 B,B 在執行過程中又調用了 C,C 執行完畢返回,B 執行完畢返回,最后是 A 執行完畢。這種方法、函數、子程序的調用方式都是通過 棧 實現的,一個線程就是執行一個子程序。子程序調用總是一個入口,一次返回,調用順序是明確的。而協程的調用和子程序不同。協程看上去也是子程序,但執行過程中,在子程序內部可中斷,然后轉而執行別的子程序,在適當的時候再返回來接着執行。
public void coroutineA() { System.out.println("1"); System.out.println("2"); System.out.println("3"); } public void coroutineB() { System.out.println("a"); System.out.println("b"); System.out.println("c"); }
上面代碼如果由協程來執行,那么在執行 coroutineA 的過程中,可以隨時中斷,去執行 coroutineB,coroutineB 也可能在執行過程中中斷再去執行 coroutineA,所以,最終的結果可能是:
1 a b 2 c 3
但是,在上面的代碼中,並沒有在 coroutineA 方法中調用 coroutineB。執行結果就像兩個線程在並發執行。但其實,通過協程執行用的是一個線程,只不過這個線程看起來有點“到處亂跑”。
協程優勢
協程 vs 線程 比較有以下 3 個重要的優勢:
1、減少了線程切換的成本。線程,不管是創建還是切換,都需要較高的成本。子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯。這也就是說,協程的效率比較高
2、協程的第二大優勢就是沒有並發問題,不需要多線程的鎖機制,因為只有一個線程,也不存在同時寫變量沖突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多
3、協程更輕量級。創建一個線程棧大概需要 1M 左右,而協程棧大概只需要幾 K 或者幾十 K。
有優勢也有劣勢,因為前面的程序看起來在“上串下跳”,所以,協程看起來沒那么好控制。
協程認知注意
1、對於操作系統來說只有進程和線程,協程的控制由應用程序顯式調度,非搶占式的
2、協程的執行最終靠的還是線程,應用程序來調度協程選擇合適的線程來獲取執行權
3、協程適合於 IO 密集型場景,這樣能提高並發性,比如請求接口、Mysql、Redis 等的操作
4、協程並不是說替換異步,協程一樣可以利用異步實現高並發。
5、協程要利用多核優勢就需要比如通過調度器來實現多協程在多線程上運行,這時也就具有了並行的特性。如果多協程運行在單線程或單進程上也就只能說具有並發特性。