再談協程

若是你對如下幾個問題有疑問,那麼本文可能會有所幫助。html

  1. 什麼是協程,或者說爲何會有協程這個概念?
  2. 怎麼用?何時須要用?
  3. 都有並行的意味,那麼協程和多線程有什麼區別?二者可否相互替代?
  4. 協程底層的實現原理。

1.2.3
linux

談協程繞不開線程,按傳統還得從進程談起,不過我想業內人員對進程和線程應該是耳熟能詳,這裏就簡單歸納下。git

進程擁有本身獨立的堆和棧,既不共享堆,亦不共享棧,進程由操做系統調度;線程擁有本身獨立的棧,共享堆(也能夠有本身的私有域),不共享棧,線程亦由操做系統調度。一個進程能夠有多個線程。github

多線程一直以來是面試必考點,雖然[web]服務端開發人員彷佛曆來不用直接操做線程,實際上是由於框架幫忙維護了,開發人員只須要關心業務實現。這也致使了部分人對多線程的某些概念模糊不清。好比關於多線程的效率:在多核cpu下,多個線程能夠並行運行在不一樣內核上,效率高;而在單核cpu中,多個線程的並行執行實際上是一個錯覺,由於它們都是運行在一個內核上,一個cpu內核同一時間只能執行一個進程/線程,所以在一個內核上的多線程執行其實效率反而比串行執行低,只是給用戶一種併發的錯覺,反而增長了線程切換的時間。web

可是效率的高低還要看線程佔用cpu資源的佔用率,好比存在大量IO操做,IO比較慢。也就是說,若是隻有單線程,那麼一旦涉及到IO操做,線程可能會被阻塞,程序的其他邏輯就只能傻等,就算那些邏輯不依賴於這個IO操做,此時線程對CPU的使用爲0,CPU就是空閒狀態。若是是多線程,是線程瓶頸,那麼其他線程則可使用cpu,而非等待IO結束。面試

題外話,一個空循環就能讓cpu滿載,參看 爲何一個空的死循環會讓CPU佔用達到100%算法

後來,出現了多路複用之類的技術,原先須要等待IO返回的線程也不須要等了,能夠和其它線程同樣忙別的事,IO返回時獲得通知再處理接下去的事情。Java的NIO和.Net的async/await就是這麼幹的。編程

通常來講,爲了不線程頻繁建立銷燬帶來的性能問題,程序裏都會使用到線程池。windows

然而仍是在單核的場景下,事情彷佛變得有點詭異。既然線程們如今都能心無旁騖地使用CPU計算,而前面也說了,一個cpu內核同時只能運行一個線程,管理多線程又是搶佔式,又是棧切換,維護生命週期啥的,影響性能不說,徹底沒得必要嘛,爲何不僅用一個線程完成全部的計算呢。什麼,你說可能須要[僞]並行計算?那就讓線程本身來安排咯,畢竟具體邏輯方面,線程自己(或者說開發人員)比CPU要清楚的多,知道何時該幹什麼,何時切換邏輯,何時不切換,都由線程本身說了算。因而,協程粉墨登場。api

協程主要是針對單線程的一個概念(如Js、NodeJs、Python因爲GIL致使的僞多線程),能夠將其看做線程運行時片斷。和線程相似,雖然貌似多個協程能夠並行執行,一個時間仍然只能運行一個。因此,若是業務邏輯是順序相關(串行)或者各任務對反饋及時性要求不高,那麼不必用協程,就跟不必多線程同樣。協程對比線程,除了有更好的性能外,還讓開發人員對執行片斷有了更好的掌控。好比Go語言,經過阻塞條件(time.sleep()、select{}等),咱們能夠手動將控制權轉移給其它的 Go 協程 , 也能夠說是告訴調度器讓它去調度其它可用空閒的 Go 協程(Go如何判斷這是阻塞代碼還沒有研究過);或者經過channel調度指定協程。

Go默認狀況下只用單線程。這就是說,你即便開了幾百個goroutine,系統中同一時間在跑的只有一個線程,也就是一個協程。依據上面的內容,你們能夠思考下Go爲什麼默認如此。咱們能夠經過 runtime.GOMAXPROCS() 設置的是Go語言能跑幾個線程,講道理,CPU幾核跑幾個線程比較合理,使用 runtime.NumCPU() 查看內核數。

在編程層面來講,協程的概念偏向於以同步編程的模式實現異步處理的編程模式,避免了多層回調代碼嵌套的問題。

其實在不少年之前,協程已經被提出了,如今只是它煥發生機的階段。


4

上文說了,協程之間應該是非順序相關的,即它們的上下文沒有強依賴關係,是相對獨立的。這裏的上下文指的就是當前的運行棧空間,它包括了參數、局部變量、各寄存器的值等內容。在協程切換的時候,咱們要想辦法將對應的上下文投射到當前線程的運行棧中,即讓線程執行特定的上下文。很容易想到malloc一塊臨時內存存放掛起的協程上下文信息,resume的時候再覆蓋回去,運行棧在內存中只有一處,這就是stackless模式。相對的還有stackful模式,在這種模式下,每一個協程都有本身的棧空間,運行棧指的就是當前協程的棧空間。現有語言的實現中,Python, Kotlin等定義的就是stackless協程, Go語言中實現的是stackful協程。

對於其它沒有在語言層面直接支持協程的語言來講,因爲協程涉及到底層的[堆]棧切換控制,所以很難單純依靠現有語法構建算法的方式實現。有人作過此類嘗試(可參看Coroutines in C),但也沒有實用性。

能直接操做執行堆棧並暴露api的,如今市面上的語言以C/C++最爲流行,基於它們也有不少開源的協程庫。下面介紹幾種實現方式。

協程分爲非對稱協程和對稱協程。在非對稱協程中,調用者和被調用者的關係是固定的,調用者將控制流轉到被調用者,被調用者運行完畢後只能返回到調用者,而不能返回到其餘協程。對稱協程則否則。對稱協程能夠很容易由非對稱協程來表達。且按通常的調用邏輯,A調B,B應返回到A,再由A發起到C的調用,而非B直接返回到C。所以,目前大多數協程庫都只實現非對稱協程。

  • 一種是藉助glibc的ucontext,及相關的四個函數getcontext、setcontext、makecontext、swapcontext,如雲風的庫。固然這隻能在linux環境下使用,在windows下,能夠藉助fiber實現相似的協程庫;
  • 利用C標準庫<setjmp.h>中的setjmp、longjmp實現協程。須要注意的是,setjmp僅負責保存寄存器的值,不負責維護其函數調用棧,這個須要另外實現;
  • 遵循規範從頭實現。如libaco,它支持 Intel386 和 x86-64 兩個平臺的Sys V ABI,並提供了非對稱協程的實現。關於Sys V ABI,It is today the standard ABI used by the major Unix operating systems such as Linux, the BSD systems, and many others. The Executable and Linkable Format (ELF) is part of the System V ABI. 也就是說,該協程庫只支持類unix系統;
  • 使用匯編實現。較爲著名的是Boost庫,協程實現有兩套:Corountine2和Corountine。Corountine2在Boost v1.59被引入,Boost.Corountine目前已被標記爲deprecated。Boost.Corountine2使用了Boost.Context,所以要使用Boost.Corountine2,必須先編譯Boost.Context。通用的C庫tbox的協程模塊也參照了Boost的實現。

關於彙編語法的平臺差別,類Unix下采用的是AT&T的彙編語法格式,Dos/Windows下面採用的是Intel彙編語法格式。

 

參考資料:

雲風-coroutine源碼解析

System V ABI

Golang 協程調度

 

 

轉載請註明本文出處:http://www.javashuo.com/article/p-hcsueoiu-gh.html

相關文章
相關標籤/搜索