協程
c++20的協程三大標簽:“性能之優秀”,“開發之靈活”,“門檻之高”
在講解c++的協程使用前,我們需要先明白協程是什么,協程可以理解為用戶態的線程,它需要由程序來進行調度,如上下文切換與調度設計都需要程序來設計,并且協程運行在單個線程中,這就成就了協程的低成本,更直接一點的解釋可以說,協程就是一種可以被掛起與恢復的特殊函數。
協程并不會真正地“脫離”當前的線程,它只是讓控制流從一個函數流轉到另一個地方,然后再回來。這個過程是 輕量級 和 非阻塞 的。
協程可以分為兩種類型對稱型與非對稱型協程:
- 非對稱協程:控制權的轉移是單向的,總是從調用者協程轉移到被調用者協程,并且被調用者協程必須將控制權返回給調用者協程。
- 對稱協程:控制權可以在任意兩個協程之間直接轉移。協程在執行過程中可以選擇將控制權交給其他任何協程,而不依賴于特定的調用層次關系。
c++協程的基本概念
c++提供的協程是典型的非對稱協程,c++中的協程有兩個特定條件:
- 協程函數必須包含至少一個協程關鍵字(
co_await
、co_yield
或co_return
)- co_await: 主要用于暫停協程的執行,同時等待某個異步操作完成。在協程執行到
co_await
表達式時,它會將控制權交還給調用者,待異步操作完成后,協程再恢復執行。 - co_yield: 主要用于生成器模式,它會產生一個值,然后暫停協程的執行。每次調用生成器的迭代器時,協程會恢復執行,直到下一個
co_yield
或者協程結束。 - co_return: 用于結束協程的執行,并返回一個值(如果協程有返回類型)。當協程執行到
co_return
時,它會銷毀自身的棧幀,并將控制權交還給調用者。
這里先簡單介紹協程的基本概念和使用,后面會結合更多代碼解釋每一個點
- co_await: 主要用于暫停協程的執行,同時等待某個異步操作完成。在協程執行到
- 返回值必須是實現了promise_type結構體的一個類或者結構體;
struct Task {struct promise_type {Task get_return_object() { return {}; }std::suspend_never initial_suspend() { return {}; }std::suspend_never final_suspend() noexcept { return {}; }void return_void() {}void unhandled_exception() {}};
};Task task() {std::cout << "Starting coroutine operation..." << std::endl;co_await std::suspend_always{};std::cout << "Coroutine operation completed." << std::endl;
}int main() {task();std::cout << "Main function continues..." << std::endl;return 0;
}// 輸出
Starting coroutine operation...
Main function continues...
這個代碼簡單演示了協程的使用,我們可以看到輸出只有兩條,并沒有輸出Coroutine operation completed.
,這個程序的運行流程是main函數調用task后,先保存當前上下文,再將當前線程的上下文切換成task的,接下來輸出一條內容運行到co_await時,再次保存當前上下文,不過這次會將上下文切換回協程的調用者也就是main,我們后續沒有再回到task運行最后一條命令,這里也就沒有相關輸出了
協程函數的返回類型有特殊要求。返回類型主要用于表示協程的句柄或包裝器,它提供了一種方式來管理協程的生命周期,以及與協程進行交互。promise_type
主要用來定義協程的生命周期和行為。
promise_type
必須實現以下關鍵方法:get_return_object()
:該函數在協程開始執行之前被調用,其作用是返回協程的返回對象initial_suspend()
:在協程開始執行時調用,用于決定協程是否在開始時就暫停。std::suspend_never
表示協程不會在開始時暫停,會立即執行。final_suspend()
:在協程結束執行時調用,用于決定協程是否在結束時暫停。std::suspend_never
表示協程不會在結束時暫停,會立即銷毀。unhandled_exception()
:處理未捕獲的異常。
下面的方法可選
return_void()
當調用co_return時會觸發這個函數,它觸發于final_suspend()
之前yield_value()
主要用于生成器返回值
我們再來明確一下調用時刻,get_return_object()
是在協程運行前就被調用了,當存放task();
這一行時就會調用get_return_object()
函數,因為我們這個演示比較簡單不會返回如何東西,我們會在下面的演示中詳細介紹,initial_suspend()
是在協程開始執行時調用,這個時刻是介于get_return_object()
與真正開始運行之間的時候,這些函數名必須一模一樣
生成器
template<typename T>
struct Generator {struct promise_type {T value_;auto get_return_object() { return Generator{this}; }auto initial_suspend() noexcept { return std::suspend_always{}; }auto final_suspend() noexcept { return std::suspend_always{}; }void unhandled_exception() { std::terminate(); }auto yield_value(T val) {value_ = val;return std::suspend_always{};}};using Handle = std::coroutine_handle<promise_type>;Handle handle_;explicit Generator(promise_type* p) : handle_(Handle::from_promise(*p)) {}~Generator() { if (handle_) handle_.destroy(); }T next() {if (!handle_.done()) handle_.resume();return handle_.promise().value_;}
};// 斐波那契生成器
Generator<int> fibonacci() {int a = 0, b = 1;while (true) {co_yield a;int temp = a;a = b;b = temp + b;}
}int main() {auto fib = fibonacci();for (int i = 0; i < 10; ++i) {std::cout << fib.next() << " "; }
}// 輸出
0 1 1 2 3 5 8 13 21 34
這個代碼使用了一種叫做生成器的特殊類,它需要我們自己實現,作用是獲得一次協程的返回值,fibonacci中使用co_yield返回了a,之前我們介紹過co_yield它的作用是,產生一個值然后暫停協程的執行
這里的promise_type結構體多了兩個東西,yield_value()
函數的作用是把傳入的值存儲到 value_
中,并且返回 std::suspend_always{}
,使得協程暫停。當使用co_yield關鍵字時就相當于調用了這個函數,T value_
用于存放我們的返回值
promise_type結構體外的東西,都是我們可以自定義的這里最重要的就是,代碼中的std::coroutine_handle<>
,它是一個協程句柄表示一個協程實例的句柄,允許開發者手動控制協程的恢復、銷毀或訪問其內部狀態,它通常實現為一個指針,直接指向協程的狀態幀 ,C++20 協程機制中的核心工具類,用于直接操作協程的生命周期和執行流程。它提供了一種底層但靈活的方式來管理協程的狀態。我們介紹幾個常用的函數
Handle::from_promise()
這是一個靜態成員函數,用于從promise_type
對象創建一個協程句柄。在協程的promise_type
中,通常會使用這個函數來獲取協程句柄,以便在返回對象中保存。resume()
恢復協程的執行。如果協程當前處于暫停狀態,調用這個函數會讓協程從暫停點繼續執行,直到下一個暫停點或者協程結束。destroy()
銷毀協程,釋放協程占用的資源。在協程執行完畢或者不再需要時,應該調用這個函數來避免內存泄漏。bool done()
檢查協程是否已經完成。如果協程已經執行完畢,返回true
;否則返回false
。promise_type& promise()
返回與協程句柄關聯的promise_type
對象的引用。通過這個引用,你可以訪問和修改協程的狀態和數據。
next()
函數用于返回值并恢復協程的執行,這里我們通過get_return_object()
構建并返回我們的生成器
自定義可等待對象
自定義可等待對象需要滿足特定的接口要求,主要涉及await_ready
、await_suspend
和await_resume
這三個成員函數。
await_ready
:用于判斷是否可以立即恢復協程的執行。若返回true
,協程會馬上恢復;若返回false
,協程就會被掛起。await_suspend
:在協程掛起時調用,這里可以執行異步操作。在示例中,使用一個新線程來模擬異步操作,操作完成后恢復協程。await_resume
:在協程恢復執行時調用,返回異步操作的結果。
template <typename T>
struct FutureAwaitable {std::future<T>& future;bool await_ready() const noexcept {return future.wait_for(std::chrono::seconds(0)) == std::future_status::ready;}void await_suspend(std::coroutine_handle<> handle) const {std::thread([this, handle]() mutable {future.wait();handle.resume();}).detach();}T await_resume() {return future.get();}
};template <typename T>
FutureAwaitable<T> co_awaitable(std::future<T>& future) {return {future};
}struct AsyncTask {struct promise_type {std::promise<int> promise;std::future<int> result = promise.get_future();auto get_return_object() { return AsyncTask{this}; }auto initial_suspend() { return std::suspend_never{}; }auto final_suspend() noexcept { return std::suspend_always{}; }void return_value(int val) {promise.set_value(val);}void unhandled_exception() {std::terminate();}};using Handle = std::coroutine_handle<promise_type>;Handle handle_;explicit AsyncTask(promise_type* p) : handle_(Handle::from_promise(*p)) {}~AsyncTask() {if (handle_) {handle_.destroy();}}int get() {return handle_.promise().result.get();}
};AsyncTask simulate_async_io() {auto future = std::async([] {std::this_thread::sleep_for(std::chrono::seconds(1));return 42;});auto result = co_await co_awaitable(future);co_return result;
}int main() {auto task = simulate_async_io();std::cout << "Waiting..." << std::endl;std::cout << "Result: " << task.get() << std::endl; // 輸出: 42return 0;
}
這個代碼演示了co_await結合自定義等待對象,實現的異步調用,當我們觸發co_await時會等待異步操作的完成,并獲得返回值,在等待時協程會掛起讓出cpu資源
在await_suspend()
函數中我們在另一個線程中恢復了協程,協程的執行線程由恢復它的線程決定。handle.resume()
是恢復協程執行的關鍵操作,它會從協程上次掛起的位置接著執行。由于 handle.resume()
是在新創建的 std::thread
線程里被調用的,所以協程恢復后會繼續在這個新線程里執行后續代碼。
return_value()
與yield_value()
函數類似,它是用來處理co_return關鍵字的返回值的