天天看點

[C++] asio + C++20協程asio + C++20協程

文章目錄

  • 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對協程的支援分兩個階段

  1. 首先是将回調風格接口的簡單包裝成可用協程表達的awaitable對象
  2. 然後是獨立出executor概念
  3. 最後是将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檔案自行浏覽, 需要補充的内容相當多, 此處暫略

該方法要求回調式接口中能傳入發起請求時的一個上下文指針, 如不能則隻能使用全局變量

繼續閱讀