天天看點

手把手了解C++20協程的編譯實作考慮下面的協程代碼總結一下

考慮下面的協程代碼

#include <iostream>
#include <coroutine>

using namespace std;

class Resumable
{

};

Resumable func() {
    cout << "hello";
    co_await std::suspend_always();
    cout << " world";
}


int main()
{
    
}

           

編譯報錯

error: unable to find the promise type for this coroutine
   13 |     co_await std::suspend_always();
      |     ^~~~~~~~
           

為什麼?

其實編譯器在編譯時,會希望生成如下的代碼:

/* 經過編譯器優化後的 func 函數 */
Resumable func()
{
    Frame *frame = operator new(size);	//	size = 函數形參大小 + 局部變量大小
    Rumable::promise_type promise;
    coroutine_handle *handle = coroutine_handle<>::from_promise(&promise);
    Resumable res = promise.get_return_object();	//	call the Resumable constructor 

    co_await promise.initial_suspend();	//	in some ways, this is a coroutine constructor
    try {
        //  func-body
        cout << "hello";
        co_await std::suspend_always();
        cout << " world";
        //	func-body end
    }catch (...) {
        promise.unhandled_exception();	//	coroutine exception handle
    }
    co_await promise.final_suspend();	//	in some ways, this is a coroutine destructor

    return res;
}
           

通過上面的代碼,可以引出兩個問題:

  1. 已知協程co_await可以完成上下文切換,那這個函數中co_await具體是怎麼調用的?
  2. promise_type 哪裡來?
  3. Resubmable如何實作?

同樣,從上面的代碼中可以推出,promise_type至少應該含有以下代碼:

class promise_type
{
public:
    auto get_return_object();
    auto initial_suspend();
    void unhandled_exception();
    auto final_suspend();
    void return_void();
};
           

抱着上面三個問題,看看Resumable的實作規範。

Resumable的編譯實作

class Resumable
{
public: /* 使用者自定義實作部分 */
    class promise_type;	// 見上個代碼塊
    
	/* 
		使用者的其他自定義實作代碼
	*/

};
           

解決上面提出的問題:

  1. 已知協程co_await可以完成上下文切換,那這個函數中co_await具體是怎麼調用的?
先繼續存疑
  1. promise_type 哪裡來?
答:從 Resumable 中由使用者手動定義而來,且必須實作一些特定方法。
  1. Resubmable如何實作?
答:Resumable 必須包含 promise_type 子類型(typedef也算),其餘沒什麼講究。

再提出一些新問題:

  1. 協程如何将一個值從函數内

    co_await

    到函數外?
  2. 看起來Resumable在編譯優化後的func裡沒有被用到,隻在最後傳回的時候return了一下,為什麼不用promise直接代替Resumable?

    換句話說:為什麼要給promise加一層外套作為傳回類型?C++為什麼要這樣設計?

以下是未解決問題清單:

  1. 已知協程co_await可以完成上下文切換,那這個函數中co_await具體是怎麼調用的?
  2. C++為什麼要采用給promise加一層外套作為傳回類型這樣的設計方式?

總結一下

從上面可以看出,co_await 之類的協程關鍵字依然存在,這說明此處的編譯優化并不是針對協程的,那為什麼要這樣做呢?

答案是為了更好的管理協程,可以看到,一次小小的協程函數調用覆寫了誕生、運作、錯誤處理、消亡等各個部分,這為将來高可用的架構奠定了基礎,但對于寫hello world的人不得不說,真***複雜。

C++有一個設計規範,叫做一個人隻做一件事,在這裡promise_type用來管理協程的生命周期。Resumable用來作為傳回值。

如果說上面講的都是協程規範的話,那麼接下來要講的部分就是具體協程的實作,看看 co_await 到底是如何調用的?

Awaitable對象的實作規範

回到最初的起點:

Resumable func() {
    cout << "hello";
    co_await std::suspend_always();
    cout << " world";
}
           

這裡

co_await std::suspend_always();

調用的是标準庫

coroutine

中的函數,直接來看看他的實作:

// 17.12.5 Trivial awaitables
/// [coroutine.trivial.awaitables]
struct suspend_always
{
  constexpr bool await_ready() const noexcept { return false; }

  constexpr void await_suspend(coroutine_handle<>) const noexcept {}

  constexpr void await_resume() const noexcept {}
};
           

這就是

co_await expr

的通用标準實作,寫的再通俗點就是:

class Awaiter
{
public:
    bool await_ready();
    auto await_suspend(coroutine_handle<> handle);
    auto await_resume();
};
           
  • await_ready

    :該任務是否已經完成?若未完成則将調用

    await_suspend

  • await_suspend

    :是否中止該任務?若中止則該協程将調用權傳回給 caller 。

    該函數傳回bool、void、coro_handle三種之一,對于不同的傳回值編譯器提供了不同的實作。

    • void

      :程式内部調用

      handle.resume()

      以繼續運作協程。
    • bool

      :傳回true表示同意中止,否則繼續執行
    • std::coroutine_handle<>

      :調用該handle的resume,随後調用權傳回 caller
  • await_resume

    :用于傳回協程值,可以是任意類型

編譯器會通過以下兩種路徑對 await 語句進行優化:

(下面我用僞代碼表示了

await_suspend

在不同傳回值下的編譯代碼)

在調用co_await 等協程關鍵字的位置,程式的操作大概類似這樣:

if(!a.await_ready()) {


# if await_suspend_return_void
	try {
		result = a.await_suspend(handle);
# if !(await_suspend_return_bool && await_suspend_return_coroutine_handle)
		return_to_caller();
#endif
	} catch(...) {
		excpetion = std::current_exception();
		goto resume_point;
	}
	
#elif await_suspend_return_bool
if(!result)
	goto resume_point;
return_to_caller();

#elif await_suspend_return_coroutine_handle
result.resume();
return_to_caller();

# endif

resume_point:
if(exception)
	std::rethrow_exception(exception);
return a.await_resume();
}

           

到這裡,編譯器完成了對co_await關鍵字的優化,接下來看個執行個體。

range

衆所周知,python裡有這樣一個函數可以這樣用:

for i in range(0, 10):
    print(i, end=' ')
           

Out:

0 1 2 3 4 5 6 7 8 9 
           

這個函數本質上可以用 python 協程這樣實作:

def my_range(low, high):
    print('are you ok?')
    while low < high:
        yield low
        low += 1


iter = my_range(0, 10)
while True:
    try:
        print(iter.send(None))
    except StopIteration:
        break
           

Out:

0 1 2 3 4 5 6 7 8 9 
           

如果用CPP20呢?首先寫出來自己想要的執行代碼,然後再考慮如何實作,說得高大上點,用測試驅動開發。

我們最終想要的是這樣的效果:

int main()
{
    Resumable iter = range(low, high);
    while(true) {
        try{
          cout << iter.get() << " ";
          iter.resume();
        }catch (...) {
            break;
        }
    }
}
           

具體實作:

#include <coroutine>
#include <iostream>
#include <string>

using namespace std;


class Awaiter
{
public:
    Awaiter(int val):val(val) {  }
    bool await_ready() { return false;}
    void await_suspend(coroutine_handle<> handle) {  }
    void await_resume() {  }

    int val;
};


class Resumable
{
public:
    class promise_type
    {
    public:
        auto get_return_object() { return Resumable(Handle::from_promise(*this)); }
        auto initial_suspend() noexcept { return std::suspend_never(); }
        auto final_suspend() noexcept { return std::suspend_never(); }
        void unhandled_exception() { throw; }
        void return_void() { }
        Awaiter await_transform(Awaiter awaiter) {
            cur_val = awaiter.val;
            return awaiter;
        }

        int cur_val = 0;
    };

    typedef coroutine_handle<promise_type> Handle;
    Resumable(Handle handle):handle(handle) {}

    void resume() { handle.resume(); }
    int get() { return handle.promise().cur_val; }

private:
    Handle handle;
};


Resumable range(int low, int high)
{
    while(low < high) {
        co_await Awaiter(low++);
    }
}


int main()
{
    int low = 0, high = 10;
    Resumable iter = range(low, high);
    while(true) {
        try{
            cout.flush() << iter.get() << " ";
            iter.resume();
        }catch (...) {
            break;
        }
    }
}
           

Out:

0 1 2 3 4 5 6 7 8 9 
           

當然,我上面為了學習了解是以強行将

awaiter

作了一個産出器,實際上這個工作應該交由

co_yield

來完成,他會調用

promise_type.yield_value(expr)

,可以直接從promise中拿到數值,更為簡便。

具體的例子可參考cpp reference 裡的這篇文章。

HelloWorld的協程實作

考慮下面的代碼:

async_void func()
{
	cout << "hello ";
	co_await std::suspend_always();
	cout << "world" << endl;
}


int main()
{
	auto f = func();
	f.resume();
}
           

經過協程優化以後:

async_void coro_func()
{
    Frame *frame = operator new(size);	//	size = 函數形參大小 + 局部變量大小
	async_void::promise_type promise;
	async_void ret = promise.get_return_objet();

	int status = 0;
	void resume() {
		switch(status) {
		case 0:
			return f0();
		case 1:
			return f1();
		}
	}
	void f0() { status = 1; cout << "hello "; }
	void f1() { cout << "world" << endl; }

	return ret;
}
           

寫得有點累了,先發這麼多,如果有人看就繼續往下寫。

class async_void
{
public:
	class promise_type {
	public:
	    auto get_return_object() { return async_void{Handle::from_promise(*this)}; }
	    auto initial_suspend() { return std::suspend_never(); }
	    void unhandled_exception() { throw; }
	    auto final_suspend() { return std::suspend_never(); }
	    void return_void() {}
	};
	
	typedef std::coroutine_handle<promise_type> Handle;
	explicit async_void(Handle h):handle(h) {}
	
	Handle handle;
	bool resume() {
		if(!handle.done())
			handle.resume();
		return !handle.done();
	}
}

async_void func()
{
	cout << "hello ";
	co_await std::suspend_always();
	cout << "world" << endl;
}

int main()
{
	auto coro = func();
	while(coro.resume());
}
           
async_void func()
{
    Frame *frame = operator new(size);				//	size = 函數形參大小 + 局部變量大小
    async_void::promise_type promise;
    coroutine_handle *handle = coroutine_handle<>::from_promise(&promise);
    async_void res = promise.get_return_object();	//	call the Resumable constructor 

    co_await promise.initial_suspend();				//	in some ways, this is a coroutine constructor
    try {
        cout << "hello ";
        co_await std::suspend_always();
        cout << "world" << endl;
    }catch (...) {
        promise.unhandled_exception();				//	coroutine exception handle
    }
    co_await promise.final_suspend();				//	in some ways, this is a coroutine destructor

    return res;
}
           
auto a = std::suspend_always();
if(!a.await_ready()) {
	try {
		a.await_suspend(handle);
		return_to_caller();
	}catch(...) {
		exception = std::current_exception();
		goto resume_point();
	}
}

resume_point:
	if(exception)
		std::rethrow_exception(exception);
	return a.await_resume();
           
async_void hi() {
	cout << "h";
	co_await std::suspend_always();
	cout << "i";
}

async_void func() {
	cout << "hello";
	co_await hi().await;
	cout << "world";
}

int main() {
	auto coro1 = hi();
	auto coro2 = func();
	coro1.resume();
	coro2.resume();
}
           

參考

  1. cpp reference
  2. 阿裡安龍飛的視訊
  3. 知乎啟蒙帖

繼續閱讀