如果你對以下幾個問題有疑問,那麼本文可能會有所幫助。 1.2.3 談協程繞不開線程,按傳統還得從進程談起,不過我想業內人員對進程和線程應該是耳熟能詳,這裡就簡單概括下。 進程擁有自己獨立的堆和棧,既不共用堆,亦不共用棧,進程由操作系統調度;線程擁有自己獨立的棧,共用堆(也可以有自己的私有域),不共用 ...
如果你對以下幾個問題有疑問,那麼本文可能會有所幫助。
- 什麼是協程,或者說為什麼會有協程這個概念?
- 怎麼用?什麼時候需要用?
- 都有並行的意味,那麼協程和多線程有什麼區別?兩者能否相互替代?
- 協程底層的實現原理。
1.2.3
談協程繞不開線程,按傳統還得從進程談起,不過我想業內人員對進程和線程應該是耳熟能詳,這裡就簡單概括下。
進程擁有自己獨立的堆和棧,既不共用堆,亦不共用棧,進程由操作系統調度;線程擁有自己獨立的棧,共用堆(也可以有自己的私有域),不共用棧,線程亦由操作系統調度。一個進程可以有多個線程。
多線程一直以來是面試必考點,雖然[web]服務端開發人員似乎從來不用直接操作線程,其實是因為框架幫忙維護了,開發人員只需要關心業務實現。這也導致了部分人對多線程的某些概念模糊不清。比如關於多線程的效率:在多核cpu下,多個線程可以並行運行在不同內核上,效率高;而在單核cpu中,多個線程的並行執行其實是一個錯覺,因為它們都是運行在一個內核上,一個cpu內核同一時間只能執行一個進程/線程,因此在一個內核上的多線程執行其實效率反而比串列執行低,只是給用戶一種併發的錯覺,反而增加了線程切換的時間。
但是效率的高低還要看線程占用cpu資源的占用率,比如存在大量IO操作,IO比較慢。也就是說,如果只有單線程,那麼一旦涉及到IO操作,線程可能會被阻塞,程式的其餘邏輯就只能傻等,就算那些邏輯不依賴於這個IO操作,此時線程對CPU的使用為0,CPU就是空閑狀態。如果是多線程,是線程瓶頸,那麼其餘線程則可以使用cpu,而非等待IO結束。
題外話,一個空迴圈就能讓cpu滿載,參看 為什麼一個空的死迴圈會讓CPU占用達到100%。
後來,出現了多路復用之類的技術,原先需要等待IO返回的線程也不需要等了,可以和其它線程一樣忙別的事,IO返回時得到通知再處理接下去的事情。Java的NIO和.Net的async/await就是這麼乾的。
一般來說,為了避免線程頻繁創建銷毀帶來的性能問題,程式里都會使用到線程池。
然而還是在單核的場景下,事情似乎變得有點詭異。既然線程們現在都能心無旁騖地使用CPU計算,而前面也說了,一個cpu內核同時只能運行一個線程,管理多線程又是搶占式,又是棧切換,維護生命周期啥的,影響性能不說,完全沒得必要嘛,為什麼不只用一個線程完成所有的計算呢。什麼,你說可能需要[偽]並行計算?那就讓線程自己來安排咯,畢竟具體邏輯方面,線程本身(或者說開發人員)比CPU要清楚的多,知道什麼時候該乾什麼,什麼時候切換邏輯,什麼時候不切換,都由線程自己說了算。於是,協程粉墨登場。
協程主要是針對單線程的一個概念(如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彙編語法格式。
參考資料:
轉載請註明本文出處:https://www.cnblogs.com/newton/p/11104187.html