標籤:

開源一個超簡單的無棧協程原型(5)

這裡我們提一個非常具有普遍性的問題:

如何處理await超時?

其實這裡有2種非常常見的方案,分別是「推」和「拉」:

  1. 只有葉子協程在調度器中註冊超時時間,在調度器因為超時而resume自己時主動結束自己(需要調度器resume自己之前設置一個超時標識)。
  2. 每個協程維護自己的超時時間,每次await子協程的時候都在調度器註冊超時時間(註冊的時間等於初始超時時間減去已經消耗的時間),所以超時之後調度器會resume_on_timeout自己。 如果發現此時自己在await子協程超時,則給這些子協程resume_on_timeout, 遞歸的把子子孫孫的流程全部結束掉。

可以看到方案1是一種很傳統的方案,類似網路阻塞IO發生錯誤或者超時,系統返回一個錯誤碼一樣。

而方案2則顯得很「現代化」(其實也沒啥新意),類似於網路阻塞IO發生了錯誤(例如SIGPIPE,往對端關閉的socket寫數據),系統用一個信號去打斷進程,然後進程轉入信號處理函數一樣。 所以這種resume_on_timeout 應該去遞歸執行特殊的,每個協程單獨寫的一個成員函數。各個子協程回收掉自己需要手工回收的資源即可。所以resume_on_timeout需要跟蹤協程內部的狀態,根據協程處在哪個await狀態,回收對應的資源(如果所有資源都在協程的第一個狀態分配,最後一個狀態回收,那麼就只需要簡單的全部回收即可)。

我個人推薦使用方案1。為什麼呢?

  1. 簡單,而且和已有的API很合拍。對超時的封裝就是普通的函數返回錯誤碼。
  2. 不需要另外維護一套類似異常處理的resume_on_timeout 回收函數。處理超時的代碼可以寫在主體函數內。我認為這是優點,分開寫真的不是解耦而是造成2個函數分開去理解同一套狀態邏輯。這裡需要內聚。
  3. 父協程不會侵入子協程的流程,進而父協程也不需要記憶自己await了哪些子協程。兩兩相安。
  4. 保護了「resume只能從子協程往父協程驅動」這個原則,不製造理解困難和驚奇。

壞處就是:

  1. 父協程的await超時時間一定不能小於串列await的子協程組的最大超時時間之和(就是說父協程可能第一次await 協程a, 第二次await協程b和c, 那麼,和就是a+max(b,c)),否則就有可能超時返回的時候, 實際已經消耗了多於這個超時時間的時間(因為父協程並沒有真正的在調度器註冊超時)。有時候這並不方便, 不能利用子協程並不是個個都把時間用到最大值,這種實際的情況。
  2. 其次,就是不能自上而下的去取消子協程的執行。resume_on_timeout 背後是一種取消操作的執行。 GRPC的產生動機和設計原則 這裡有介紹,"。。當任務因果鏈被追蹤時,取消可以級聯。客戶端可能會被告知調用超時,此時服務就可以根據客戶端的需求來調整自己的行為。" 類似mysql等後台操作也支持某種程度的事務回滾和取消。

我的回答是

  1. 實際中需要強行設置await操作鏈上每一個await操作的操作時間上限嗎(麻煩又瑣碎,還需要層層遞減)? 筆者的有棧和無棧協程後台工作經歷都不這樣使用,相反,只去設置了葉子協程本身的超時時間就滿足了我們的需求。經常是父協程構造好了葉子協程,然後傳遞給子協程去用。。從實際工作的角度來說,這是直面問題的本質?手工設置好葉子節點組 超時時間和不要大於自己的處理時間上限,這個世界就太平了。。
  2. 取消操作有固然很好,但是不支持也真的不是大事。即使最應該加入取消操作的前端,很多「取消」操作都不能如你所願。你試下複製幾萬個文件,然後中途點取消,能把已複製的文件刪除嗎?取消是非常複雜而容易弄出問題來的,輕易的承諾能夠「完整取消」會給你的系統帶來巨大的包袱,如同c++程序需要做到完美的「異常安全」一樣。後台高耗時的操作,輕易的去執行取消,有時候還會帶來分散式數據不一致,腦裂等其他問題。。不同人對取消的操作級別如果執行不一致,還會帶來很多溝通上的問題。

綜上,我的cort_proto原型協程庫里,直接斬斷了父協程向子協程投遞任何異常終止的路徑。

既然父已經await了子,那麼父就不應該對子突然再次產生輸入了(實際上偷偷去修改子的成員變數我都挺鄙視的)。 個人認為簡化了問題,保證了執行流程總是符合我們預期的,AWAIT後面的代碼總是能夠被執行的,利大於弊。

各位看官,你們怎麼看呢?

推薦閱讀:

協程調度時機一:系統調用
使用coroutine實現狀態機(2)
協程和纖程的區別?
libco協程庫上下文切換原理詳解

TAG:C | 协程 |