C++中的yield和fork
各位看官,您沒有看錯,C++是可以有yield和fork的,這個主題小麥很早以前就打算寫,只是一直沒有一個契機給我這個動力。前段日子,小麥幫朋友處理一個用單線程模擬多線程的活兒的時候,再次想到了這個事情,決定寫一下,也算是自己的一個回顧。本文其實也可以叫做boost::asio::coroutine原理解析。。。
當然,C++本身是沒有yield這個關鍵字的,這個關鍵字的意義可以參考C#對其的定義,就是跳出當前的執行過程,下次調用這個過程的時候,直接從yield處開始執行。例如
int foo() {yield return 1;yield return 2;yield return 3; }連續調用foo函數3次,分別得到了1,2,3這三個數,這就是yield的基本語義,而fork的意思是保存當前上下文,跳轉到目標函數,在目標函數執行完成之后,回到當前位置,例如
std::cout<<"line 1"<<std::endl; fork foo()從上面的例子可以看出,yield和fork的作用就相當于用單線程模擬了多線程的語義。這個語義正是常說的coroutine,即協程。通常來說,協程的實現是保存當前上下文,然后在線程內不斷切換上下文,這種情況也叫做stackfull coroutine.本文要描述的是不保存上下文的情況,即stackless coroutine。
怎么用
如果各位看過C#中關于 yield的實現的話,可能會有一些印象,C#把yield語句翻譯成了一個狀態機,利用狀態機記錄每次進入函數時應該從何處開始執行。然而在C++中,失去了編譯器的支持,想實現這個語義還是頗為困難的。下面的實現是boost asio的作者給出的實現,其逆天程度之高,以致于這個實現最開始并沒有出現在boost asio的官方文檔中,而是在boost asio的example中靜靜的躺著,直到最近的boost 1.54,這個技術才正式成為boost asio的一部分,
簡而言之,這個實現是用宏實現的,請各位忘記C++的程序跳轉,記住C#的yield,我們來看個例子
#include <boost/asio/yield.hpp> #include <boost/asio/coroutine.hpp> #include <iostream>boost::asio::coroutine c;void foo(int i) {reenter(c){yield std::cout<<"foo1 "<<i<<std::endl;fork foo(100);yield std::cout<<"foo2 "<< i+1<<std::endl;} } int main() {foo(1);foo(2);foo(3);return 0; }這個程序的輸出是。。。
foo1 1 foo2 101 foo2 3這個咋看之下有點難以理解,我們先忽略具體的細節,解釋一下這是怎么回事。
首先,我們需要聲明一個corountine,然后將需要重復進入的代碼用reenter包括起來。第一次調用foo的時候,代碼執行到第一個yield,此時,foo直接返回。
第二次調用的時候,程序直接執行上一次yield之后的代碼,即fork,此時,程序調用foo,需要注意的是,被調用的fork不再執行第一個yield,而是直接從當前語句開始執行,于是得到輸出foo2 101,返回之后,程序調用fork之后的yield,得到輸出foo2 3。
第三次調用的時候,由于不再有任何未執行的yield,因此不再產生任何輸出。
很自然的,我們會想像C#中的yield一樣使用,類似這樣
int nums() {reenter(c){for(int i = 0; i < 10; i++){yield return i;}} }很遺憾,這是不行的,這會有一個編譯錯誤,Switch case is in protected scope,這顯然讓人覺得不可思議,聯想到我們寫的程序本身就不太像正常的C++代碼,我們有必要詳細了解一下到底發生了什么。
內部實現
首先,我們先從能看見的類開始,coroutine。
class coroutine{public:/// Constructs a coroutine in its initial state.coroutine() : value_(0) {}/// Returns true if the coroutine is the child of a fork.bool is_child() const { return value_ < 0; }/// Returns true if the coroutine is the parent of a fork.bool is_parent() const { return !is_child(); }/// Returns true if the coroutine has reached its terminal state.bool is_complete() const { return value_ == -1; }private:friend class detail::coroutine_ref;int value_;};namespace detail {class coroutine_ref{public:coroutine_ref(coroutine& c) : value_(c.value_), modified_(false) {}coroutine_ref(coroutine* c) : value_(c->value_), modified_(false) {}~coroutine_ref() { if (!modified_) value_ = -1; }operator int() const { return value_; }int& operator=(int v) { modified_ = true; return value_ = v; }private:void operator=(const coroutine_ref&);int& value_;bool modified_;};} // namespace detail上面這兩個類,都很簡單,從類名就可以看出來這是啥意思,略過不提。重要的是如何用宏實現reenter、yield、fork。
reenter的定義是一個宏,名字有重定義,如下
#define BOOST_ASIO_CORO_REENTER(c) \switch (::boost::asio::detail::coroutine_ref _coro_value = c) \case -1: if (_coro_value) \{ \goto terminate_coroutine; \terminate_coroutine: \_coro_value = -1; \goto bail_out_of_coroutine; \bail_out_of_coroutine: \break; \} \else case 0:回想一下,reenter的用法是
reenter(coroutine){ your_code...;}因此your_code實際被放在case 0:中,回想一下_coro_value最初的初始化值是0,因此,第一次確實執行了your_code。
yield的定義同樣是個宏,定義如下
#define BOOST_ASIO_CORO_YIELD_IMPL(n) \for (_coro_value = (n);;) \if (_coro_value == 0) \{ \case (n): ; \break; \} \else \switch (_coro_value ? 0 : 1) \for (;;) \case -1: if (_coro_value) \goto terminate_coroutine; \else for (;;) \case 1: if (_coro_value) \goto bail_out_of_coroutine; \else case 0:其中n的定義為__COUNTER___。
看到這個宏,估計看官和我最初反應差不多,深切的懷疑這個代碼的合法性,因為有這樣一種混亂的用法
switch(n) {for(...)case 1: if(...) }這個事實上就是將case 1看作普通的類似goto使用的代碼標簽,也就是說可以通過switch到達,也可以通過for到達!
這里用戶的代碼仍然在case 0中出現,當執行這段代碼是,_coro_value的值首先被設置成一個程序標識用的整數n, 在最內層的swithc-case中,_coro_value的值為n,因此,程序跳轉到case 0,首先執行用戶的代碼,用戶代碼執行完后,返回執行for循環,此時執行對應的if語句,即goto bail_out_of_coroutine,這個定義在之前的reenter的宏中,此時將直接break,返回整個reenter的最外層。
再次調用函數時,由于_coro_value的值仍然為n,則直接調用case 0,直接執行后續的語句。
fork的實現如下,這個小麥就不再具體解釋,各位看官請自行理解,比較簡單。
#define BOOST_ASIO_CORO_FORK_IMPL(n) \for (_coro_value = -(n);; _coro_value = (n)) \if (_coro_value == (n)) \{ \case -(n): ; \break; \} \else至此,我們就理解了前面的程序為什么會出現一個編譯錯誤,因為大量的使用了switch-case,因此程序的層次很重要,不能在reenter中隨意修改 yield和fork的層次,否則會導致switch-case不匹配,從而出現編譯錯誤!!
總結
上述的用法并不是唯一的用法,各位還可以繼承boost::asio::coroutine,而不是定義變量,更多用法可以參考boost::asio的example。當然也可以和小麥探討哦~
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的C++中的yield和fork的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: c++中boost协程5种使用实例
- 下一篇: 谈谈对协程的理解