以下是 Boost 1.66.0 的更新示例,基于 Tanner 的出色回答:
#include <iostream> // std::cout, std::endl
#include <chrono> // std::chrono::seconds
#include <functional> // std::bind
#include <thread> // std::thread
#include <utility> // std::forward
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
template <typename Signature, typename CompletionToken>
auto async_add_one(CompletionToken token, int value) {
// Initialize the async completion handler and result
// Careful to make sure token is a copy, as completion's handler takes a reference
using completion_type = boost::asio::async_completion<CompletionToken, Signature>;
completion_type completion{ token };
std::cout << "Spawning thread" << std::endl;
std::thread([handler = completion.completion_handler, value]() {
// The handler will be dispatched to the coroutine's strand.
// As this thread is not running within the strand, the handler
// will actually be posted, guaranteeing that yield will occur
// before the resume.
std::cout << "Resume coroutine" << std::endl;
// separate using statement is important
// as asio_handler_invoke is overloaded based on handler's type
using boost::asio::asio_handler_invoke;
asio_handler_invoke(std::bind(handler, value + 1), &handler);
}).detach();
// Demonstrate that the handler is serialized through the strand by
// allowing the thread to run before suspending this coroutine.
std::this_thread::sleep_for(std::chrono::seconds(2));
// Yield the coroutine. When this yields, execution transfers back to
// a handler that is currently in the strand. The handler will complete
// allowing other handlers that have been posted to the strand to run.
std::cout << "Suspend coroutine" << std::endl;
return completion.result.get();
}
int main() {
boost::asio::io_context io_context;
boost::asio::spawn(
io_context,
[&io_context](boost::asio::yield_context yield) {
// Here is your coroutine
// The coroutine itself is not work, so guarantee the io_context
// has work while the coroutine is running
const auto work = boost::asio::make_work_guard(io_context);
// add one to zero
const auto result = async_add_one<void(int)>(yield, 0);
std::cout << "Got: " << result << std::endl; // Got: 1
// add one to one forty one
const auto result2 = async_add_one<void(int)>(yield, 41);
std::cout << "Got: " << result2 << std::endl; // Got: 42
}
);
std::cout << "Running" << std::endl;
io_context.run();
std::cout << "Finish" << std::endl;
}
输出:
Running
Spawning thread
Resume coroutine
Suspend coroutine
Got: 1
Spawning thread
Resume coroutine
Suspend coroutine
Got: 42
Finish
备注:
- 充分利用 Tanner 的回答
- 首选网络 TS 命名(例如 io_context)
- boost::asio 提供了一个 async_completion 类,它封装了处理程序和 async_result。当处理程序引用 CompletionToken 时要小心,这就是现在显式复制令牌的原因。这是因为通过 async_result (
completion.result.get) 让出将使相关的 CompletionToken 放弃其底层强引用。这最终可能导致协程意外提前终止。
- 明确指出单独的
using boost::asio::asio_handler_invoke 语句非常重要。显式调用可以防止调用正确的重载。
-
我还要提到,我们的应用程序以两个 io_context 结束,协程可以与之交互。具体来说,一个用于 I/O 绑定工作的上下文,另一个用于 CPU。使用带有boost::asio::spawn 的显式链最终为我们提供了对协程运行/恢复的上下文的明确控制。这帮助我们避免了零星的 BOOST_ASSERT(!is_running()) 失败。
使用显式链创建协程:
auto strand = std::make_shared<strand_type>(io_context.get_executor());
boost::asio::spawn(
*strand,
[&io_context, strand](yield_context_type yield) {
// coroutine
}
);
调用显式调度到链(多 io_context 世界):
boost::asio::dispatch(*strand, [handler = completion.completion_handler, value] {
using boost::asio::asio_handler_invoke;
asio_handler_invoke(std::bind(handler, value), &handler);
});
-
我们还发现,在 async_result 签名中使用未来允许异常传播回协程恢复。
using bound_function = void(std::future<RETURN_TYPE>);
using completion_type = boost::asio::async_completion<yield_context_type, bound_function>;
产量为:
auto future = completion.result.get();
return future.get(); // may rethrow exception in your coroutine's context