C++ coroutine-ts 怎麼用-Part 3 從generator看co_yield怎麼用

clang和MSVC的最新實現已經提供了試驗性的協程實現,想要試用的話,clang須要開啓-fcoroutins-ts -stdlib=libc++兩個開關,而MSVC須要開啓/await開關。c++

咱們從一個簡單的generator來看編譯器作了什麼:promise

#include <experimental/generator>
#include <cstdio>

namespace stdexp = std::experimental;

stdexp::generator<int> gen(int count) {
    for (int i = 0; i < count; i++) {
        co_yield i;
    }
}

int main() {
    for (auto v : gen(10)) {
        printf("%d\n", v);
    }
}

顯然的,這段代碼會輸出0-9,若是你以爲main裏面的range-based-for有點玄學的話,這麼寫main也行。函數

int main() {
    auto g = gen(10);
    auto it = g.begin();
    printf("%d\n", *it); //0
    it++;
    printf("%d\n", *it); //1
    it++;
    printf("%d\n", *it); //2
}

第一次調用gen,生成了一個generator g,而後咱們獲取了g的迭代器,每次對迭代器取值都能得到一個從gen函數裏yield出來的值,而對迭代器的++,不難猜出,它負責恢復gen函數的執行。spa

若是你爲所欲爲的刪除或者增長代碼裏的it++,就能發現輸出的結果會響應的跳過一些值,不難猜出,每次經過it++恢復gen函數執行,gen函數yield以後,都會把yield的值存在某個地方,而後你才能經過*it的操做把它取出來。設計

promise類型

這個某個地方,其實就是promise類型和返回值的共同做用結果,對於每一個協程,在正式進入函數體時,編譯器都會構造一個promise_type的對象,這個promise_type按照C++的尿性,必然是你本身提供的,經過一個traits,這個咱們後面會講。而後編譯器會調用promise的get_return_object方法,這個方法的返回值在本例中會被用來構造generator對象,也就是說coroutine的返回值是經過promise提供的,爲何要這樣設計,由於coroutine常常須要在每次suspend/resume以前/後修改或設置返回值,好比本例的存一個int進去。因此須要讓promise和返回值之間創建一個聯繫,這樣最好的方式就讓promise來提供返回值,這樣promise_type和return_type的內部實現之間就能夠搞一些py交易,好比互相保存一下對方的指針什麼的,來實現互相操做對方。指針

這麼多話,總結一下,就是coroutine經過操做promise來修改返回值,promise是coroutine向外返回的結果的入口。code

同時promise_type也是整個協程的抽象,最先咱們提到過編譯器實現的無棧協程會把整個協程分配在某個地方,而這裏的promise,就成爲了編譯器向你暴露的協程的一個接口。協程

函數究竟是怎麼執行的

最開始,編譯器會構造gen函數的promise_type對象,經過coroutine_traitspromise_type類型。coroutine_traits接受函數返回類型和參數類型做爲參數,你能夠本身特化它。對象

stdexp::coroutine_traits<stdexp::generator<int>, int>::promise_type __p;

咱們這個例子裏返回類型是generator<int>,有一個參數int接口

而後編譯器調用__p.get_return_object(),用他的返回值(多半是這個promise自己)來構造main裏的generator g

g的構造函數會經過(編譯器)標準庫提供的coroutine_handle<promise_type>::from_promise(__p),從promise獲取對應的coroutine_handle,這是表明協程的句柄,用它來控制協程的恢復和銷燬。

而後編譯器調用__p.initial_suspend(),它的返回值用來決定要不要在函數體執行前暫停,在咱們這個例子裏,應該暫停,由於函數體應當在第一次獲取迭代器(即調用begin時開始執行)。

而後函數被暫停,調用回到main,第一行執行完畢。

當你調用g,即那個generator<int>begin後,g__handle.resume()恢復協程執行,gen函數進入循環體。

gen函數第一次co_yield時,編譯器調用__p.yield_value(i),將局部變量i的值傳給promisepromise就能夠把這個值存起來,等待main裏面generator的迭代器來取,而後yield_value的返回值用來決定要不要暫停協程執行,咱們這個例子裏,一樣應該暫停,回到調用者那裏處理yield出來的值。

此時調用回到main,第二行執行完畢,g的迭代器裏已經裝了一個gen函數yield出來的值。

printf裏面*itit經過generator裏面存的coroutine_handle把值取出來,coroutine_handle能夠經過promise成員函數來獲取到對應的promise

it++一樣也會恢復協程執行,和上面begin的描述同樣。

當你第11次調用it++時,gen函數其實是第12次恢復執行(由於begin恢復執行了一次),gen函數從循環最後一次yield的位置恢復,退出循環,執行到函數體的結尾,此時編譯器會調用__p.final_suspend(),詢問你是否是要在最後再暫停一次gen函數,咱們這裏是須要的。由於在控制流從gen的循環退出,執行到函數最後時,若是協程直接結束,coroutine_handle直接被銷燬,it無從得知它的__handle已經變爲野指針,當他用__handle.done()來更新本身是否到end的狀態時就會爆炸,因此最後應該再額外暫停一次,就像迭代器容許指向一個尾後位置來表示end同樣,容許一個協程在函數體執行完畢,銷燬以前再暫停一下,表示一個end狀態。

main最後generator g銷燬時會順帶執行__handle.destroy(),銷燬gen協程。

用僞代碼說一下

generator<int> gen(int a) {
    coroutine_traits<generator<int>, int>::promise_type __p;
    generator<int> __r = __p.get_return_object(); //這裏其實是構造了main裏的返回值,可是代碼裏無法描述,就寫在gen裏了
    if (__p.initial_suspend()) { //true
        //第1次暫停
    }
    for (int i = 0; i < count; i++) {
        if (__p.yield_value(i)) { //true
            //第i+2次暫停
        }
    }
    if (__p.final_suspend()) { //true
        //第count+2次暫停
    }
    //__p銷燬
}
相關文章
相關標籤/搜索