chriskohlhoff / asio

Asio C++ Library
http://think-async.com/Asio
4.84k stars 1.2k forks source link

Absence of the pass through await_transform() method for promise type #752

Open ghost opened 3 years ago

ghost commented 3 years ago

@vignatyuk commented on Mar 18, 2020, 8:25 PM UTC:

Absence of the pass through await_transform() method for an unknown awaitable types blocks usage of the user defined coroutine friendly completion handlers.

I propose to change the only available pass through await_transform() method from:

template template auto boost::asio::awaitable_frame_base::await_transform(awaitable<T, Executor> a) const { return a; }

to the

template template <typename T, typename = std::enable_if_t> decltype(auto) boost::asio::awaitable_frame_base::await_transform(T&& a) const noexcept { return std::forward(a); }

Second default argument through std::enable_if_t<> may be removed if new await_transform() method does not break any other overloads. There is no standard is_awaitable_v check, so it should be defined somewhere, though it's easy.to implement.

This issue was moved by chriskohlhoff from boostorg/asio#331.

jhlgns commented 2 years ago

I second this, I would like to retrieve an boost::system::error_code instead of catching an exception when reading or writing. I see a simple solution which involves defining a custom awaitable:

auto asyncRead(tcp::socket &socket, boost::asio::mutable_buffer buffer) {
    struct Awaitable
    {
        struct Result
        {
            boost::system::error_code errorCode;
            std::size_t bytesRead;
        };

        tcp::socket &socket;
        boost::asio::mutable_buffer buffer;
        Result result;

        bool await_ready()
        {
            return false;
        }

        void await_suspend(std::coroutine_handle<> awaitingCoroutine)
        {
            this->socket.async_read_some(
                this->buffer,
                [this, awaitingCoroutine](boost::system::error_code errorCode, std::size_t bytesRead) mutable {
                    this->result.errorCode = errorCode;
                    this->result.bytesRead = bytesRead;
                    awaitingCoroutine.resume();
                });
        }

        Result await_resume()
        {
            return this->result;
        }
    };

    return Awaitable{.socket = socket, .buffer = buffer};
};

...

boost::asio::awaitable<void> echo(tcp::socket socket)
{
    try {
        char buffer[1024];
        while (true) {
            auto result = co_await asyncRead(socket, boost::asio::buffer(buffer));

            if (result.errorCode) {
                std::cout << "async_read: " << result.ec.message() << std::endl;
            } else {
                co_await boost::asio::async_write(socket, boost::asio::buffer(buffer, result.bytesRead), boost::asio::use_awaitable);
            }
        }
    } catch (std::exception &ex)    {
        std::cout << "echo exception: " << ex.what() << std::endl;
    }
}

The problem here is the missing pass-through await_transform(...) of boost::asio::awaitable<> I guess.

ned14 commented 2 years ago

The OP's proposed added overload above doesn't work with how ASIO dispatches coroutines, which involves pumping a stack and with which each awaitable needs to register for things to work.

What would work however is replacing await_transform(awaitable<T, Executor>) with reference passthroughs:

    template <typename T>
    awaitable<T, Executor>& await_transform(awaitable<T, Executor>& a) const {
      if(attached_thread_->entry_point()->throw_if_cancelled_)
        if(!!attached_thread_->get_cancellation_state().cancelled())
          do_throw_error(boost::asio::error::operation_aborted, "co_await");
      return a;
    }

    template <typename T>
    awaitable<T, Executor>&& await_transform(awaitable<T, Executor>&& a) const {
      if(attached_thread_->entry_point()->throw_if_cancelled_)
        if(!!attached_thread_->get_cancellation_state().cancelled())
          do_throw_error(boost::asio::error::operation_aborted, "co_await");
      return std::move(a);
    }

Now people can create their own awaitable types inheriting from awaitable<T, Executor>, and write their own custom logic into await_ready(), await_suspend() and await_resume(). This is very useful for writing efficient state machines whereby you specifically know which ASIO coroutine in the stack needs resuming, which then saves ASIO doing a linear pass over all suspended coroutines in the awaitable thread's stack polling each to decide which ought to be resumed.

showeeks commented 2 years ago

The OP's proposed added overload above doesn't work with how ASIO dispatches coroutines, which involves pumping a stack and with which each awaitable needs to register for things to work.

What would work however is replacing await_transform(awaitable<T, Executor>) with reference passthroughs:

    template <typename T>
    awaitable<T, Executor>& await_transform(awaitable<T, Executor>& a) const {
      if(attached_thread_->entry_point()->throw_if_cancelled_)
        if(!!attached_thread_->get_cancellation_state().cancelled())
          do_throw_error(boost::asio::error::operation_aborted, "co_await");
      return a;
    }

    template <typename T>
    awaitable<T, Executor>&& await_transform(awaitable<T, Executor>&& a) const {
      if(attached_thread_->entry_point()->throw_if_cancelled_)
        if(!!attached_thread_->get_cancellation_state().cancelled())
          do_throw_error(boost::asio::error::operation_aborted, "co_await");
      return std::move(a);
    }

Now people can create their own awaitable types inheriting from awaitable<T, Executor>, and write their own custom logic into await_ready(), await_suspend() and await_resume(). This is very useful for writing efficient state machines whereby you specifically know which ASIO coroutine in the stack needs resuming, which then saves ASIO doing a linear pass over all suspended coroutines in the awaitable thread's stack polling each to decide which ought to be resumed.

Could you please give me an example? What's the correct way of inheriting awaitable and customizing await_suspend logic?