Boost.Asio堆栈无关的协程,具有不同的CompletionToken签名

huangapple go评论67阅读模式
英文:

Boost.Asio stackless coroutine with different CompletionToken signatures

问题

Boost.Asio的异步函数具有不同的CompletionToken签名。例如,boost::asio::async_writeWriteToken

void write_handler(
    const boost::system::error_code& ec,
    std::size_t bytes_transferred)

boost::asio::ip::tcp::resolver::async_resolveResolveToken

void resolve_handler(
    const boost::system::error_code& ec,
    boost::asio::ip::tcp::resolver::results_type results);

第一个参数类型都是const boost::system::error_code&,但第二个参数类型不同。

Stackless Coroutine使用的operator()的签名与一系列异步操作的CompletionToken签名匹配。

然而,async_write()async_resolve具有不同的CompletionToken签名。

如何以最佳方式编写具有不同CompletionToken签名的stackless coroutine代码?

环境:C++17,Boost 1.82.0

我尝试了什么:

  1. 使用std::any
// 代码示例已略过
  1. 使用带有if constexpr的模板
// 代码示例已略过
  1. 准备多个operator()()

以上是您提供的内容的翻译。如果您需要更多信息或有其他问题,请随时提出。

英文:

Question

Boost.Asio's asynchronous functions have various CompletionToken signature. For example, boost::asio::async_write has WriteToken.

void write_handler(
    const boost::system::error_code& ec,
    std::size_t bytes_transferred)

boost::asio::ip::tcp::resolver::async_resolve has ResolveToken.

void resolve_handler(
    const boost::system::error_code& ec,
    boost::asio::ip::tcp::resolver::results_type results);

The first argument types are both const boost::system::error_code& but the second argument types are different.

Stackless Coroutine uses operator() that's sigunature matchs the series of async operations CompletionToken signature.

However, async_write() and async_resolve have different CompletionToken signature.

What is the best way to write the stackless coroutine code with different CompletionToken signature ?

Environment

C++17
Boost 1.82.0

What I tried

I wrote resolve-connect-write code using several way. So far, std::any seems good but perhaps there would be better way.

Use std::any

It requires std::any_cast. If the type is mismatched, then exception is thrown. not bad.

#include <iostream>
#include <any>

#include <boost/asio.hpp>

#include <boost/asio/yield.hpp>

struct myapp {
    myapp(
        boost::asio::ip::tcp::resolver& res,
        boost::asio::ip::tcp::socket& sock
    ): res{res}, sock{sock}
    {}

    void operator()(
        boost::system::error_code const& ec = boost::system::error_code{},
        std::any second = std::any{}
    ) {
        reenter (coro) {
            // resolve
            yield res.async_resolve("localhost", "1883", *this);
            std::cout << "async_resolve:" << ec.message() << std::endl;
            if (ec) return;

            // connect
            yield {
                auto results = std::any_cast<boost::asio::ip::tcp::resolver::results_type>(second);
                boost::asio::async_connect(
                    sock,
                    results.begin(),
                    results.end(),
                    *this
                );
            }
            std::cout << "async_connect:" << ec.message() << std::endl;
            if (ec) return;

            // write
            yield {
                auto buf = std::make_shared<std::string>("hello");
                boost::asio::async_write(
                    sock,
                    boost::asio::buffer(*buf),
                    boost::asio::consign(
                        *this,
                        buf
                    )
                );
            }
            std::cout << "async_write:"
                      << ec.message()
                      << " bytes transferred:"
                      << std::any_cast<std::size_t>(second)
                      << std::endl;
        }
    }

    boost::asio::ip::tcp::resolver& res;
    boost::asio::ip::tcp::socket& sock;
    boost::asio::coroutine coro;
};

#include <boost/asio/unyield.hpp>

int main() {
    boost::asio::io_context ioc;
    boost::asio::ip::tcp::resolver res{ioc.get_executor()};
    boost::asio::ip::tcp::socket sock{ioc.get_executor()};
    myapp ma{res, sock};
    ma(); // start coroutine
    ioc.run();
}

Use template with if constexpr

No cast required. Template instantiated multiple times, but coro based switch case seems to work well.

#include <iostream>
#include <type_traits>

#include <boost/asio.hpp>

#include <boost/asio/yield.hpp>

struct myapp {
    myapp(
        boost::asio::ip::tcp::resolver& res,
        boost::asio::ip::tcp::socket& sock
    ): res{res}, sock{sock}
    {}

    template <typename Second = std::nullptr_t>
    void operator()(
        boost::system::error_code const& ec = boost::system::error_code{},
        Second&& second = nullptr
    ) {
        reenter (coro) {
            // resolve
            yield res.async_resolve("localhost", "1883", *this);
            std::cout << "async_resolve:" << ec.message() << std::endl;
            if (ec) return;

            // connect
            yield {
                if constexpr(
                    std::is_same_v<std::decay_t<Second>, boost::asio::ip::tcp::resolver::results_type>
                ) {
                    boost::asio::async_connect(
                        sock,
                        second.begin(),
                        second.end(),
                        *this
                    );
                }
            }
            std::cout << "async_connect:" << ec.message() << std::endl;
            if (ec) return;

            // write
            yield {
                auto buf = std::make_shared<std::string>("hello");
                boost::asio::async_write(
                    sock,
                    boost::asio::buffer(*buf),
                    boost::asio::consign(
                        *this,
                        buf
                    )
                );
            }
            if constexpr(
                std::is_same_v<std::decay_t<Second>, std::size_t>
            ) {
            std::cout << "async_write:"
                      << ec.message()
                      << " bytes transferred:"
                      << std::any_cast<std::size_t>(second)
                      << std::endl;
            }
        }
    }

    boost::asio::ip::tcp::resolver& res;
    boost::asio::ip::tcp::socket& sock;
    boost::asio::coroutine coro;
};

#include <boost/asio/unyield.hpp>

int main() {
    boost::asio::io_context ioc;
    boost::asio::ip::tcp::resolver res{ioc.get_executor()};
    boost::asio::ip::tcp::socket sock{ioc.get_executor()};
    myapp ma{res, sock};
    ma(); // start coroutine
    ioc.run();
}

Prepare multiple operator()()

One of the big advantage of stackless coroutine is continuous code. This approach lose the advantage so I don't choose it.

答案1

得分: 2

IMHO,正确的答案是要有多个operator()的重载版本,这一点您已经一概拒绝了。 (下文提供了理由)。

当然,您也可以兼顾两者:

void operator()()                                   { return call();                      } 
void operator()(error_code ec, size_t n)            { return call(ec, n);                 } 
void operator()(error_code ec, endpoints eps)       { return call(ec, 0, std::move(eps)); } 
void operator()(error_code ec, endpoint /*unused*/) { return call(ec, 0);                 } 

私有部分:

private:
    void call(
        error_code ec = {},
        size_t bytes_transferred = {},
        std::optional<endpoints> eps = {})
    {
        reenter(coro)
        {

详细内容请参考链接:在Coliru上查看

英文:

IMHO the correct answer is to have multiple overloads of operator(), which you dismiss out of hand. (rationale below¹)

You might of course have the best of both worlds:

    void operator()()                                   { return call();                      } 
void operator()(error_code ec, size_t n)            { return call(ec, n);                 } 
void operator()(error_code ec, endpoints eps)       { return call(ec, 0, std::move(eps)); } 
void operator()(error_code ec, endpoint /*unused*/) { return call(ec, 0);                 } 
private:
void call(
error_code ec = {},
size_t bytes_transferred = {},
std::optional&lt;endpoints&gt; eps = {})
{
reenter(coro)
{

See it Live On Coliru²

#include &lt;iostream&gt;
#include &lt;optional&gt;
#include &lt;boost/asio.hpp&gt;
#include &lt;boost/asio/yield.hpp&gt;
namespace asio = boost::asio;
using asio::ip::tcp;
struct myapp {
using error_code = boost::system::error_code;
using endpoint = tcp::endpoint;
using endpoints = tcp::resolver::results_type;
myapp(
tcp::resolver&amp; res,
tcp::socket&amp; sock
): res{res}, sock{sock}
{
}
void operator()()                                   { return call();                      } 
void operator()(error_code ec, size_t n)            { return call(ec, n);                 } 
void operator()(error_code ec, endpoints eps)       { return call(ec, 0, std::move(eps)); } 
void operator()(error_code ec, endpoint /*unused*/) { return call(ec, 0);                 } 
private:
void call(
error_code ec = {},
size_t bytes_transferred = {},
std::optional&lt;endpoints&gt; eps = {})
{
reenter(coro)
{
// resolve
yield res.async_resolve(&quot;localhost&quot;, &quot;1883&quot;, *this);
std::cout &lt;&lt; &quot;async_resolve:&quot; &lt;&lt; ec.message() &lt;&lt; std::endl;
if(ec) return;
// connect
yield asio::async_connect(sock, *eps, *this);
std::cout &lt;&lt; &quot;async_connect:&quot; &lt;&lt; ec.message() &lt;&lt; std::endl;
if (ec) return;
// write
yield {
auto buf = std::make_shared&lt;std::string&gt;(&quot;hello\n&quot;);
asio::async_write(
sock,
asio::buffer(*buf),
asio::consign(
*this,
buf
)
);
}
std::cout &lt;&lt; &quot;async_write:&quot; &lt;&lt; ec.message()
&lt;&lt; &quot; bytes transferred:&quot; &lt;&lt; bytes_transferred
&lt;&lt; std::endl;
}
}
tcp::resolver&amp; res;
tcp::socket&amp; sock;
asio::coroutine coro;
};
#include &lt;boost/asio/unyield.hpp&gt;
int main()
{
asio::io_context ioc;
tcp::socket sock{ioc};
tcp::resolver res{sock.get_executor()};
myapp ma{res, sock};
ma(); // start coroutine
ioc.run();
}

Output

g++ -std=c++20 -O2 -Wall -pedantic -pthread main.cpp
nc -lp 1883&amp; sleep 1; ./a.out; kill %1
async_resolve:Success
async_connect:Success
async_write:Success bytes transferred:6
hello

¹ The main reason is that it plays better with "stable" coroutines, where the coro itself is passed as the "self" argument which avoids copying all of the coro state, instead just moving ownership of a single (dynamic) allocation.

Another reason is that it allows me to code the state machine with overload resolution, avoiding dangerously subtle surprises around the reenter and yield macros.

² COLIRU doesn't have asio::consign or DNS service

huangapple
  • 本文由 发表于 2023年4月17日 11:14:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/76031488.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定