談談對協程的理解


什么是協程

協程是在線程之上由“用戶”構建的並發單元,對OS來說無感知,協程的切換由用戶自己管理和調度。(這里的用戶是相較於內核而言的,一些通用庫這里也理解為用戶)

 

C/C++怎么實現協程

作為一個C++后台開發,我知道像go, lua之類的語言在語言層面上提供了協程的api,但是我比較關心C++下要怎么實現這一點,下面的討論都是從C/C++程序員的角度來看協程的問題的。

boost和騰訊都推出了相關的庫,語言層面沒有提供這個東西。我近期閱讀了微信開源的libco協程庫,協程核心要解決幾個問題:

1. 協程怎么切換? 這個是最核心的問題,有很多trick可以做到這點,libco的做法是利用glibc中ucontext相關調用保存線程上下文,然后用swapcontext來切換協程上下文,libco的實現中對swapcontext的匯編實現做了一些刪減和改動,所以在性能上會比C庫的swapcontext提升1個數量級。

2. IO阻塞了怎么辦?試想在一個多協程的線程里,一個阻塞IO由一個協程發起,那么整個線程都阻塞了,別的協程也拿不到CPU資源,多個協程在一起等着IO的完成。libco中的做法是利用同名函數+dlsym來hook socket族的阻塞IO,比如read/write等,劫持了系統調用之后把這些IO注冊到一個epoll的事件循環中,注冊完之后把協程yield掉讓出cpu資源,在IO完成的時候resume這個協程,這樣其實把網絡IO的阻塞點放在了epoll上,如果epoll沒有就緒fd,那其實在超時時間內epoll還是阻塞的,只是把阻塞的粒度縮小了,本質上其實還是用epoll異步回調來解決網絡IO問題的。那么問題來了,對於一些沒有fd的一些重IO(比如大規模數據庫操作)要怎么處理呢?答案是:libco並沒有解決這個問題,而且也很難解決這個問題,首先要明確的一點是我們的目的是讓用戶只是僅僅調用了一個同步IO而已,不希望用戶感知到調用IO的時候其實協程讓出了cpu資源,按libco的思路一種可能的方法是,給所有重IO的api都hook掉,然后往某個異步事件庫里丟這個IO事件,在異步事件返回的時候再resume協程。這里的難點是可能存在的重IO這么多,很難寫成一個通用的庫,只能根據業務需求來hook掉需要的調用,然后協程的編寫中依然可以以同步的方式調用這些IO。從以上可能的做法來看協程很難去把所有阻塞調用都hook掉,所以libco很聰明的只把socket族的相關調用給hook掉,這樣可以libco就成為一個通用的網絡層面的協程庫,可以很容易移植到現有的代碼中進行改造,但是也讓libco適用場景局限於如rpc風格的proxy/logic的場景中。在我的理解里,阻塞IO讓出cpu是協程要解決的問題,但是不是協程本身的性質,從實現上我們可以看出我們還是在用異步回調的方式在解決這個問題,和協程本身無關。

3. 如果一個協程沒有發起IO,但是一直占用CPU資源不讓出資源怎么辦?無解,所以協程的編寫對使用場景很重要,程序員對協程的理解也很重要,協程不適合於處理重cpu密集計算(耗時),只要某個協程即一直占用着線程的資源就是不合理的,因為這樣做不到一個合理的並發,多線程同步模型由OS來調度並發,不存在說一個並發點需要讓出資源給另一個,而協程在編寫的時候cpu資源的讓出是由程序員來完成的,所以協程代碼的編寫需要程序員對協程有比較深刻的理解。最極端的例子是程序員在協程里寫個死循環,好,這個線程的所有協程都可以歇歇了。

 

協程有什么好處

說了這么多協程,協程的好處到底是啥?為什么要使用協程?

1. 協程極大的優化了程序員的編程體驗,同步編程風格能快速構建模塊,並易於復用,而且有異步的性能(這個看具體庫的實現),也不用陷入callback hell的深坑。

2. 第二點也是我最近一直在糾結的一點,協程到底有沒有性能提升?

1)從多線程同步模型切到協程來看,首先很明確的性能提升點在於同步到異步的切換,libco中把阻塞的點全部放到了epoll線程中,而協程線程並不會發生阻塞。其次是協程的成本比線程小,線程受棧空間限制,而協程的棧空間由用戶控制,而且實現協程需要的輔助數據結構很少,占用的內存少,那么就會有更大的容量,比如可以輕松開10w個協程,但是很難說開10w個線程。另外一個問題是很多人拿線程上下文切換比協程上下文切換開銷大來推出協程模型比多線程並發模型性能優這點,這個問題我糾結了很久。對於這個問題,我先做一個簡單的具體抽象:在不考慮阻塞的情況下,假設8核的cpu,不考慮搶占中斷優先級等因素,100個任務並發執行,100個線程並發和10個線程每個線程10個協程並發對比兩者都可以把cpu資源利用起來,對OS來說,前者100個線程參與cpu調度,后者10個線程參與cpu調度,后者還有額外的協程切換調度,先考慮線程切換的上下文,根據Linux內核調度器CFS的算法,每個線程拿到的時間片是動態的,進程數在分配的時間片在可變區間的情況下會直接影響到線程時間片的長短,所以100個線程每個線程的時間片在一定條件下會要比10個線程情況下的要短,也就意味着在相同時間里,前者的上下文切換次數是比后者要多的,所以可以得出一個結論:協程並發模型比多線程同步模型在一定條件下會減少線程切換次數(時間片有固定的范圍,如果超出這個范圍的邊界則線程的時間片無差異),增加了協程切換次數,由於協程的切換是由程序員自己調度的,所以很難說協程切換的代價比省去的線程切換代價小,合理的方式應該是通過測試工具在具體的業務場景得出一個最好的平衡點。

2)從異步回調模型切到協程模型來看,從一些已有協程庫的實現來看,協程的同步寫法卻有異步性能其實還是異步回調在支撐這個事情,所以我認為協程模型是在異步模型之上的東西,考慮到本身協程上下文切換的開銷(其實很小)和數據結構調用的一些開銷,理論上協程是比異步回調的性能要稍微差一點,但是可以處於幾乎持平的性能,因為協程實現的代價非常小。

3)從一些異步驅動庫的角度來看協程的話,因為異步框架把代碼封裝到很多個小類里面,然后串起來,這中間會涉及相當多的內存分配,而數據大都在離散的堆內存里面,而協程風格的代碼,可以簡單理解為一個簡潔的連續空間的棧內存池,輔助數據結構也很少,所以協程可能會比厚重的封裝性能會更好一些,但是這里的前提是,協程庫能實現異步驅動庫所需要的功能,並把它封裝到同步調用里。

 

如理解有偏差,歡迎指正。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM