協程是 C++20 引入的新特性。
文章目錄
- 基本概念
- std::coroutine_handle
- promise 類型
- co_yield
- 基本用法
- 優勢
- 異步 TCP
- co_await
基本概念
協程(Coroutine)是一種比線程更加輕量級的并發編程模型。協程的調度由程序員手動控制。
異步不是并行,但是不阻塞~
協程的上下文切換是在用戶態完成的,不需要陷入內核,因此切換速度非常快。
當協程遇到 co_await 時,它會保存當前的上下文信息,然后讓出執行權,待異步操作完成后再恢復上下文繼續執行。切換開銷非常小,幾乎可以忽略不計。
-
如果你在做異步編程(如網絡 I/O、文件 I/O),用 co_await
-
如果你在實現生成器(如迭代器、流式數據處理),用 co_yield
std::coroutine_handle
協程的句柄,一個模板類——對協程進行控制與管理。
std::coroutine_handle<promise_type>;
主要方法:
-
resume()
:用于讓協程恢復執行。
當協程處于掛起狀態時,調用 resume() 方法,協程就會從掛起的位置接著執行。 -
done()
:用于檢查協程是否已經執行完畢。
若協程執行完畢,done() 方法會返回 true;反之,則返回 false。 -
destroy()
:用于銷毀協程,釋放協程占用的資源。 -
promise()
:返回與協程關聯的 promise 對象(struct promise_type),通過這個對象可以訪問協程的狀態和數據。
promise 類型
promise_type 對象定義了協程的行為和狀態管理方式。
promise_type 結構體中的部分方法名字是固定的,協程框架會依據這些固定的方法名來調用相應的邏輯
固定的方法名:
get_return_object
創建并返回協程的返回對象yield_value
當協程里使用 【co_yield】 語句時,會調用此方法。它負責處理 co_yield 后面的值,并通過返回值決定協程是否要掛起。initial_suspend
在協程啟動時會調用此方法,決定了協程在開始執行時是否要立即掛起。
【返回類型
】有 std::suspend_always(表示協程開始就掛起,控制權交還給調用者)和 std::suspend_never(表示協程開始就執行,co_yield 后接著執行)final_suspend
協程即將結束時會調用該方法,決定了協程在結束時是否要掛起。
返回同 initial_suspendunhandled_exception
當協程內部拋出未被內部捕獲的異常時,會調用這個方法來處理異常。return_void
和return_value
處理協程返回值。
co_yield
co_yield 用于在協程中產生一個值,然后暫停協程的執行,將控制權交還給調用者。
這意味著協程可以在不同的時間點多次產生值,而不是像普通函數那樣一次性返回一個結果。
當調用者再次請求協程繼續執行時,協程會從 co_yield 之后的位置恢復執行。
避免了傳統異步編程中的回調地獄問題。
Generator generate_numbers() {for (int i = 0; i < 5; ++i) {co_yield i; // 生成值并掛起}
}
每執行一次 co_yield i
,協程框架就會調用 Generator 類(當前協程函數的返回值類型)中 promise_type 結構體
的 yield_value 方法
,并且把 i 的值傳遞給該方法。
基本用法
簡單生成器示例,生成從 0 到 4 的整數
#include <coroutine>
#include <iostream>
#include <cstdlib>// 生成器類定義
class Generator {
public:struct promise_type { // 【定義了協程的行為】 (包括如何初始化、掛起、恢復、處理異常以及產生值等)int current_value; // 當前生成的值Generator get_return_object() { return Generator(std::coroutine_handle<promise_type>::from_promise(*this)); }std::suspend_always initial_suspend() { return {}; } // 初始掛起std::suspend_always final_suspend() noexcept { return {}; } // 最終掛起void unhandled_exception() { std::exit(1); } // 異常處理std::suspend_always yield_value(int value) { // ※ 用于處理 co_yield 語句current_value = value;return {};}void return_void() {} // 處理 co_return 語句,這里的協程不返回值。};using handle_type = std::coroutine_handle<promise_type>;//協程的句柄,對協程進行控制與管理// 迭代器類struct Iterator {handle_type coro_handle;bool operator!=(const Iterator&) const { return !coro_handle.done(); }void operator++() { coro_handle.resume(); }int operator*() const { return coro_handle.promise().current_value; } // 獲取值};// 構造函數和析構函數 : 接收/銷毀 句柄explicit Generator(handle_type h) : coro_handle(h) {}~Generator() { if (coro_handle) coro_handle.destroy(); }// 支持范圍循環Iterator begin() {if (coro_handle) coro_handle.resume();return Iterator{coro_handle};}Iterator end() { return Iterator{nullptr}; }private:handle_type coro_handle;
};// 協程函數
Generator generate_numbers() {for (int i = 0; i < 5; ++i) {co_yield i; // 生成值并掛起}
}// 使用示例
int main() {for (auto num : generate_numbers()) {std::cout << "Generated: " << num << std::endl;}return 0;
}
優勢
- 異步非阻塞:
協程通過 co_await 掛起執行但不阻塞線程,可以在等待 I/O 時釋放 CPU 資源
單線程即可處理大量并發 I/O 請求(如網絡連接、文件讀寫)
像網絡IO,磁盤IO等,CPU只是調度,真正執行的是硬件:網卡和磁盤。
所以程序中高IO的情況下,不想阻塞IO,就可以使用協程。
生命周期結束,協程可以正常進行。
IO異常后,協程也會拋出異常。
- 低開銷
協程切換成本遠低于線程切換(通常是納秒級 vs 微秒級)。
異步 TCP
#include <boost/asio.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <coroutine>
#include <iostream>namespace asio = boost::asio;
using asio::ip::tcp;// 協程異步連接和讀寫
asio::awaitable<void> async_client() { // 該協程不返回任何值try {auto executor = co_await asio::this_coro::executor; //獲取當前協程的執行器,用于創建 tcp::socket 對象for (int i = 0; i < 5; ++i) {tcp::socket socket(executor);// 異步連接(非阻塞)co_await socket.async_connect( // 【異步連接】tcp::endpoint(asio::ip::make_address("127.0.0.1"), 8080),asio::use_awaitable // 協程掛起等, 控制權返回給事件循環,事件循環可以繼續處理其他異步操作。);// 異步發送數據std::string request = "Hello from coroutine!";co_await asio::async_write(socket, asio::buffer(request), asio::use_awaitable); // 【異步發送】// 異步接收響應char response[1024];size_t len = co_await socket.async_read_some(asio::buffer(response), asio::use_awaitable); // 【異步接收】std::cout << "Received: " << std::string(response, len) << std::endl;}} catch (const std::exception& e) {std::cerr << "Error: " << e.what() << std::endl;}
}int main() {asio::io_context io_context; // Boost.Asio 的核心對象,負責管理所有的異步操作和事件循環。for (int i = 0; i < 5; ++i) {// 啟動協程任務asio::co_spawn(io_context, async_client(), asio::detached); // (執行器, 協程函數, 協程任務獨立運行 不等待)}// 運行事件循環io_context.run(); return 0;
}
co_await
關鍵字。讓異步代碼可以以同步的形式編寫。
C++20 之前,沒有 co_await 這個關鍵字,異步編程通常借助回調函數或者 std::future 與 std::promise 來實現,難以維護。
后面通常跟著一個可等待對象(Awaitable),這個對象需要實現特定的接口:
await_ready
判斷可等待對象是否已經準備好await_suspend
當 await_ready 返回 false 時,await_suspend 方法會被調用。這個方法負責掛起協程,并安排在可等待對象完成時恢復協程的執行。await_resume
當可等待對象完成時,await_resume 方法會被調用。該方法的返回值會作為 co_await 表達式的結果返回給協程。
#include <iostream>
#include <coroutine>
#include <future>
#include <thread>// 定義一個可等待對象
struct Awaitable {bool await_ready() const noexcept { return false; }void await_suspend(std::coroutine_handle<> handle) const noexcept {std::thread([handle]() {std::this_thread::sleep_for(std::chrono::seconds(1));handle.resume();}).detach();}void await_resume() const noexcept {}
};// 協程函數
std::coroutine_handle<> coroutine_handle; // 協程句柄聲明,這里沒有使用
struct Task { // 協程函數的返回類型, 同“基本用法”的Generator類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 coroutine_function() {std::cout << "Coroutine started." << std::endl;co_await Awaitable{};std::cout << "Coroutine resumed after waiting." << std::endl;
}int main() {coroutine_function();std::cout << "Main function continues." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(2));return 0;
}