文章目錄
- asio + C++20協程
-
- 0. 前言
- 1. awaitable
-
- 1.1 原始接口
- 1.2 awaitable協程
- 1.3 co_await
- 1.4 任意和全部co_await
- 1.5 線程池
- 2. coro
-
- 2.1 啟動
- 2.2 形參要求
- 2.3 從this擷取executor
- 2.4 vs的編譯錯誤修複
- 3. 回調包裝
-
- 3.1 C++式回調
- 3.2 C式回調
asio + C++20協程
0. 前言
自C++20為止, C++主要有以下幾種風格的協程
- 回調風格
- 鍊式風格
- 仿線程風格, C++20協程就屬于此類
其中最優雅的當屬仿線程風格, 可以在一個函數中連貫地将資料流處理流程寫完.
然而, C++20語言标準直接提供的标準協程是多方大佬撕逼後妥協的結果, 高度強調自由度, 并不适合直接拿來使用.
boost中有幾個庫實作有類似協程的功能, 比如
- context: 提供執行棧切換的功能, 但不負責值的傳遞.
- coroutine, coroutine2: 仿線程風格
- asio: 正在适配C++20協程
本文介紹如何借助asio使用C++20協程
asio對協程的支援分兩個階段
- 首先是将回調風格接口的簡單包裝成可用協程表達的awaitable對象
- 然後是獨立出executor概念
- 最後是将io_context包裝成協程context對象
目前(1.24.0)第一第二階段的代碼已經移出experimental, 可用完美适配使用了, 第三階段的代碼目前還在experimental階段(甚至還存在編譯器相容性問題需要改源碼修複).
第二階段的代碼預計C++23進入标準庫std::execution, 爾後asio庫在适配完标準庫的executor後, 也将進入标準庫std::net
1. awaitable
1.1 原始接口
首先包含頭檔案asio.hpp來使用下面的東西.
#include <asio.hpp>
using namespace asio;
使用io_context可以建立一個沒有線程池的純排程上下文. 然後可以使用post或dispatch添加一項可執行任務.
io_context ctx;
post(ctx, []{
cout << "hello" << endl;
});
dispatch(ctx, []{
cout << "world" << endl;
});
post是添加到任務隊列尾部, dispatch是如果可以的話(如可在目前線程執行), 則立即執行.
post和dispatch是實作異步的原始接口.
1.2 awaitable協程
使用co_spawn來将awaitable協程包裝成任務.
co_spawn(ctx, []() -> awaitable<void> {
co_return;
}, detached);
最後在适合的線程内調用
ctx.run()
來啟動協程
awaitable協程對外隻能有傳回值
co_return
,
awaitable<T>
中
T
的類型就是
co_return
的傳回值的類型. 對内可以用
co_await
等待其他東西的結果.
1.3 co_await
以一個可await的東西timer為例
using namespace std::chrono_literals;
io_context ctx;
steady_timer timer(ctx, 3s); // 3秒倒計時
在協程内使用運算符
co_await
來等待結果(timer倒計時結束)
co_spawn(ctx, [&]() -> awaitable<void> {
co_await timer.async_wait(use_awaitable);
cout << "hello" << endl;
}, detached);
1.4 任意和全部co_await
如果有多個可await的對象, 而隻想等待其中一個, 或全部, 可使用awaitable_operators内的運算符
#include <asio/experimental/awaitable_operators.hpp>
using namespace std::experimental::awaitable_operators;
有兩個timer
steady_timer timer1(ctx, 1s);
steady_timer timer2(ctx, 2s);
協程内, 使用||運算符來等待任意一個完成, 用&&運算符來等待全部完成.
co_spawn(ctx, [&]() -> awaitable<void> {
co_await (timer1.async_wait(use_awaitable) || timer2.async_wait(use_awaitable));
cout << "hello" << endl;
}, detached);
如果
co_await co1
得到
T
類型的值,
co_await co2
得到
U
類型的值
那麼一般情況下
co_await (co1 || co2)
得到
std::variant<T, U>
類型的值.
co_await (co1 && co2)
得到
std::tuple<T, U>
類型的值.
awaitable<int> f() { co_return 123; }
awaitable<std::string> g() { co_return "hello"; }
awaitable<void> h() { co_return; }
int main() {
io_context ctx;
co_spawn(ctx, []() -> awaitable<void> {
std::tuple<int, std::string> result1 = co_await (f() && (g() && h()));
std::variant<int, std::string, std::monostate> result2 = co_await (f() || g() || h());
}, detached);
}
1.5 線程池
io_context不含執行器, 需要自行建立線程, 或直接使用主線程來啟動其任務循環.
可以使用asio提供的線程池thread_pool來取代沒有線程的io_context.
thread_pool ctx(4); // 4線程線程池
...
co_spawn(ctx, []() -> awaitable<void> {
...
}, detached);
...
ctx.join();
2. coro
2.1 啟動
相比于隻能有傳回值的awaitable, coro是真正的通用協程, 不僅可以co_return, 也可以co_yield.
co_yield可以對外發送值, 也可以從外接收值.
coro<std::string(double), int> co1(any_io_executor exec) {
double recv = co_yield "hello";
std::cout << "co recv: " << recv << std::endl;
co_return 123;
}
可以用coro協程啟動coro協程
coro<void> co2(any_io_executor exec) {
auto c1 = co1(exec);
std::string str = co_await c1(233.0);
co_return;
}
也可以用awaitable協程啟動coro協程, 使用async_resume來調起coro協程
awaitable<void> co3() {
auto exec = co_await this_coro::executor;
auto c1 = co1(exec);
std::optional<std::string> res = co_await c1.async_resume(123.0, use_awaitable);
}
2.2 形參要求
coro協程一般要求形參裡要有
any_io_executor
或是一個有
get_executor()
方法的對象, 如
io_context
或
tcp::socket
等.
awaitable<void> co4(io_context& ctx) {
co_return;
}
2.3 從this擷取executor
但如果coro協程是作為方法定義的, 其中所在的類内有get_executor方法, 那麼上述形參可以放寬為自動從所在類中擷取, 進而免于傳入.
struct MyCoro : public io_context {
coro<int> co5() {
co_yield 123;
}
}
2.4 vs的編譯錯誤修複
1.24.0版本的asio在編譯時, 會出現類似"->"類型錯誤的提示, 原因是源碼中在捕獲包中使用了和類名coro相同的變量名coro, 導緻編譯錯誤
将變量名coro改成coro以外的名字即可修複
3. 回調包裝
3.1 C++式回調
将回調接口改造成協程接口是很常見的将老接口接入協程的方式.
設有一個C++式回調接口
template<typename Callback>
void callback(int arg, Callback func) {
std::thread([func = std::move(func), arg]() mutable {
std::this_thread::sleep_for(3s);
std::move(func)(arg);
}).detach();
}
如下代碼可将其包裝為asio式接口
template<completion_token_for<void(int)> CompletionToken>
auto callbackWrapper(int arg, CompletionToken&& token) {
// 進行傳回方式變換, 允許根據token變換傳回方式, 使得可選擇use_coro, use_awaitable等傳回方式
return async_initiate<CompletionToken, void(int)>([]
(completion_handler_for<void(int)> auto handler, int arg) {
// 放置一個空的追蹤任務, 防止executor以為目前沒有任務而退出
auto work = make_work_guard(handler);
callback(arg, [handler = std::move(handler), work = std::move(work)]
(int result) mutable {
dispatch(work.get_executor(), [handler = std::move(handler), result]() mutable {
// 結束追蹤任務 傳回回調結果
std::move(handler)(result);
});
});
}, token, arg);
}
在awaitable協程中可用co_await
3.2 C式回調
可以下載下傳asio源碼, 找到examples/cpp20/operations/c_callback_wrapper.cpp檔案自行浏覽, 需要補充的内容相當多, 此處暫略
該方法要求回調式接口中能傳入發起請求時的一個上下文指針, 如不能則隻能使用全局變量