Сохранить обработчик завершения асинхронной операции boost asio

Мне нужно реализовать собственную асинхронную операцию, используя библиотеку boost asio. Саму операцию выполнит сторонняя библиотека.

Подход, которому я следую:

  1. запустите асинхронную операцию через boost::asio::async_initiate. Для этого я реализую инициирующую функцию
  2. Сохраните обработчик асинхронного завершения, который передается с помощью boost::asio::async_initiate в инициирующую функцию.
  3. Запустить стороннюю асинхронную операцию
  4. Выйти из функции запуска.
    Согласно инициирующая функция boost reference должна быть неблокирующей.
  5. Когда асинхронная операция завершится, вызовите ранее сохраненный обработчик асинхронного завершения.

Код, повторяющий это, приведен ниже. Сторонняя библиотека здесь моделируется с отдельным потоком и логическим флагом:

#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). Никак нельзя сохранить обработчик завершения. Я попробовал следующее:

  1. Используйте std::function и переместите обработчик завершения в список захвата
    Это не сработало, потому что std::function требует, чтобы обработчик был копируемым.
  2. Создайте общий указатель на обработчик завершения, который затем можно будет записать в лямбда-выражение.
  3. Сохраните обработчик завершения непосредственно в переменной.

Итак, мои вопросы:

  1. Правильно ли я реализую интеграцию?
  2. Если да, то каков подход, разработанный в boost asio для сохранения обработчика завершения во время работы?

Заранее спасибо.

std::move_only_function
Ahmed AEK 02.09.2024 14:41

Это может быть вариант. Но для этого нужно работать с C++20. К сожалению, move_only_function входит в состав C++23.

Alexander Stepaniuk 02.09.2024 14:45
boost.org/doc/libs/1_85_0/boost/asio/detail/… ... у asio есть облегченный вариант для внутреннего использования, короче говоря, просто используйте общий_ptr для обработчика, если вам необходимо использовать std::function, это не так сложно написать 10-строчную обертку со стиранием типов только для перемещения для обработчика.
Ahmed AEK 02.09.2024 14:47
std::move_only_function может работать, но Asio добавил специальный обработчик стирания типов . Это нетривиально @AhmedAEK, так что не придумывайте что-то свое, если в этом нет необходимости
sehe 02.09.2024 15:18

@sehe это зависит от того, сколько у вас функций и насколько быстро они вам нужны, move_only_function является лучшим, так как имеет 1 косвенное обращение при вызове функции, а any_completion_handler имеет 2 косвенных направления, и в 10 строках вы можете написать одну с 2 или 3 косвенными адресами.

Ahmed AEK 02.09.2024 17:05

@AhmedAEK Но он теряет всю связанную семантику исполнителя/распределителя! См., например. stackoverflow.com/a/71475865/85371 — это может привести к серьезным ошибкам.

sehe 02.09.2024 17:07
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
6
51
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы можете использовать относительно новый контейнер для стирания текста 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)); ?

Alexander Stepaniuk 02.09.2024 16:52

В общем, обработчики Asio могут быть доступны только для перемещения («Обработчик завершения — это предоставляемый пользователем объект функции, предназначенный только для перемещения, который будет вызываться не более одного раза с результатом асинхронной операции»). Технически вызов operator()(...)&& может быть квалифицирован с помощью rvalue-ref. Еще лучше было бы std::forward<decltype(handler)>(handler) подумать об этом.

sehe 02.09.2024 16:57

Если вам не нужно отслеживание работы, вы можете упростить это до coliru.stacked-crooked.com/a/e960cad1b5c50430

sehe 02.09.2024 17:05

Гениальное решение для одноразового звонка!

Alexander Stepaniuk 02.09.2024 17:07

если обработчик должен быть вызван как std::forward<decltype(handler)>(handler)(...), то, я думаю, его также следует зафиксировать в лямбда-выражении посредством идеальной пересылки: handler = std::forward<decltype(handler)>(handler) ?

Alexander Stepaniuk 02.09.2024 17:09

Действительно. Это один из редких случаев, когда иногда мне хочется использовать макрос... #define FWD(x) std::forward<decltype(x)>(x) или что-то в этом роде.

sehe 02.09.2024 17:11

Другие вопросы по теме