Мне нужно реализовать собственную асинхронную операцию, используя библиотеку boost asio. Саму операцию выполнит сторонняя библиотека.
Подход, которому я следую:
boost::asio::async_initiate
. Для этого я реализую инициирующую функциюboost::asio::async_initiate
в инициирующую функцию.Код, повторяющий это, приведен ниже. Сторонняя библиотека здесь моделируется с отдельным потоком и логическим флагом:
#include <iostream>
#include <string>
#include <thread>
#include <functional>
#include <atomic>
#include <boost/asio.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/experimental/awaitable_operators.hpp>
#include <boost/asio/experimental/co_spawn.hpp>
std::atomic<bool> asyncOpStarted = false;
using signature = void(std::string);
std::function<signature> completionHandler;
// Dummy implementation to simulate the async operation
void start_operation()
{
asyncOpStarted = true;
std::cout << "Async operation starting..." << std::endl;
}
void do_async_loop()
{
while (!asyncOpStarted) {
// sleep & check if operation started
std::cout << "Waiting for async start..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
};
std::cout << "Async operation started..." << std::endl;
// simualate delay
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Async operation finished. Invoke completion handler..." << std::endl;
completionHandler("Done");
}
// Async operation implementation
template <typename CompletionToken>
auto async_do_operation(boost::asio::io_context& io, CompletionToken&& token)
{
return boost::asio::async_initiate<CompletionToken, signature>(
[&](auto&& handler) {
// I need to store "handler" here
// This doesn't work. std::function<...> requires handler to be copyable:
//completionHandler = [h = std::move(handler)](std::string res) mutable {
// h(res);
//};
start_operation();
},
std::move(token)
);
}
int main()
{
std::thread t(do_async_loop);
boost::asio::io_context io;
boost::asio::co_spawn(io, [&]() -> boost::asio::awaitable<void> {
try {
// Use `boost::asio::use_awaitable` as the completion token
auto result = co_await async_do_operation(io, boost::asio::use_awaitable);
std::cout << "Coroutine: " << result << std::endl;
} catch (...) {
std::cerr << "Exception caught" << std::endl;
}
}, boost::asio::detached);
// Run the io_context to process async events
io.run();
}
У меня не получается выполнить шаг (2). Никак нельзя сохранить обработчик завершения. Я попробовал следующее:
std::function
и переместите обработчик завершения в список захватаstd::function
требует, чтобы обработчик был копируемым.Итак, мои вопросы:
Заранее спасибо.
Это может быть вариант. Но для этого нужно работать с C++20. К сожалению, move_only_function входит в состав C++23.
std::function
, это не так сложно написать 10-строчную обертку со стиранием типов только для перемещения для обработчика.
std::move_only_function
может работать, но Asio добавил специальный обработчик стирания типов . Это нетривиально @AhmedAEK, так что не придумывайте что-то свое, если в этом нет необходимости
@sehe это зависит от того, сколько у вас функций и насколько быстро они вам нужны, move_only_function
является лучшим, так как имеет 1 косвенное обращение при вызове функции, а any_completion_handler
имеет 2 косвенных направления, и в 10 строках вы можете написать одну с 2 или 3 косвенными адресами.
@AhmedAEK Но он теряет всю связанную семантику исполнителя/распределителя! См., например. stackoverflow.com/a/71475865/85371 — это может привести к серьезным ошибкам.
Вы можете использовать относительно новый контейнер для стирания текста asio::any_completion_handler
.
Вот упрощенная версия, которая также исправляет некоторые ошибки:
std::forward
вместо std::move
std::move
в синтезированном обработчике (в async_initiate
)#include <boost/asio.hpp>
#include <iostream>
#include <string>
#include <thread>
namespace asio = boost::asio;
std::atomic<bool> asyncOpStarted = false;
using signature = void(std::string);
asio::any_completion_handler<signature> completionHandler;
// Dummy implementation to simulate the async operation
void start_operation() {
asyncOpStarted = true;
std::cout << "Async operation starting..." << std::endl;
}
void do_async_loop() {
while (!asyncOpStarted) {
// sleep & check if operation started
std::cout << "Waiting for async start..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
};
std::cout << "Async operation started..." << std::endl;
// simualate delay
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Async operation finished. Invoke completion handler..." << std::endl;
completionHandler("Done");
}
template <typename Token> //
auto async_do_operation(asio::any_io_executor ex, Token&& token) {
return asio::async_initiate<Token, signature>(
[&](auto&& handler) {
completionHandler = //
[w = make_work_guard(ex), handler = std::move(handler)] //
(std::string res) mutable //
{ //
std::move(handler)(std::move(res));
};
start_operation();
},
std::forward<Token>(token));
}
asio::awaitable<void> coro() try {
auto ex = co_await asio::this_coro::executor;
std::cout << "Coroutine: " << co_await async_do_operation(ex, asio::deferred) << std::endl;
} catch (...) {
std::cerr << "Exception caught" << std::endl;
}
int main() {
std::thread t(do_async_loop);
asio::io_context io;
co_spawn(io, coro, asio::detached);
io.run();
t.join();
}
Выход:
Waiting for async start...
Async operation starting...
Async operation started...
Async operation finished. Invoke completion handler...
Coroutine: Done
Спасибо за ответ. И за дополнительные рекомендации. Просто любопытно, по какой причине handler
в std::move(...)
в этой строке: std::move(handler)(std::move(res));
?
В общем, обработчики Asio могут быть доступны только для перемещения («Обработчик завершения — это предоставляемый пользователем объект функции, предназначенный только для перемещения, который будет вызываться не более одного раза с результатом асинхронной операции»). Технически вызов operator()(...)&&
может быть квалифицирован с помощью rvalue-ref. Еще лучше было бы std::forward<decltype(handler)>(handler)
подумать об этом.
Если вам не нужно отслеживание работы, вы можете упростить это до coliru.stacked-crooked.com/a/e960cad1b5c50430
Гениальное решение для одноразового звонка!
если обработчик должен быть вызван как std::forward<decltype(handler)>(handler)(...)
, то, я думаю, его также следует зафиксировать в лямбда-выражении посредством идеальной пересылки: handler = std::forward<decltype(handler)>(handler)
?
Действительно. Это один из редких случаев, когда иногда мне хочется использовать макрос... #define FWD(x) std::forward<decltype(x)>(x)
или что-то в этом роде.