Asio 201 - deferred

Dec 11, 2022

Asio deferred

Aysnc operations

Asio introduced the concept of an async_operation, which describes a primary expression that can be invoked with a completion token. In C++20 this is also a language concept.

asio::io_context ctx;
asio::async_operation auto post_op = [&](auto && token){return asio::post(ctx, std::move(token));};

auto f = post_op(asio::use_future);
ctx.run();
f.get(); // void

Async operations can be used in parallel_group and directly co_awaited in C++20.

asio::deferred as a completion token

Using asio::deferred as a completion token, will give you a lazy async_operation as the result value.

asio::io_context ctx;
asio::async_operation auto post_op = asio::post(ctx, asio::deferred);

auto f = std::move(post_op)(asio::use_future);
ctx.run();
f.get(); // void

deferred expressions

Additionally, a deferred can be invoked with a function object that returns another deferred expression. E.g.:

asio::io_context ctx;
asio::async_operation auto post_op = asio::post(ctx, asio::deferred);
asio::async_operation auto double_post_op = 
    asio::post(asio::deferred([&]{return post_op;}));

auto f = std::move(double_post_op)(asio::use_future);
ctx.run();
f.get(); // void

This now will call two posts subsequently.

Not every deferred expression however is an async_operation, deferred provides multiple utilities.

deferred.values

asio.values is a deferred expression that just returns values, so that you can modify the completion signature.

asio::io_context ctx;
asio::async_operation auto post_int_op = 
    asio::post(ctx, 
        asio::deferred(
            []
            {
                return asio::deferred.values(42);
            }
        ));

auto f = std::move(post_int_op)(asio::use_future);
ctx.run();
assert(f.get() == 42); // int

This already can be useful to modify completion signatures, similar to asio::append and asio::prepend.

deferred.when

Next deferred provides a conditional, that takes two deferred expressions.

auto def = asio::deferred.when(condition).then(def1).otherwise(def2);

This can be used for simple continuations with error handling. Let’s say we want to read some memory from socket1 and write to socket2.

extern asio::ip::tcp::socket socket1, socket2;
char buf[4096];

auto forward_op = 
    socket1.async_read_some(
        buf, 
        asio::deferred(
            [&](system::error_code ec, std::size_t n)
            {
                return asio::deferred
                    .when(!!ec) // complete with the error and `n`
                    .then(asio::deferred.values(ec, n))
                    .otherwise(
                        asio::async_write(socket2,
                            asio::buffer(buf, n),
                        asio::deferred));
            }
        ));

Multiple deferreds

Since all the calls with deferred yield async_operations, we can combine more than two, just by invoking the resulting expression. Let’s say we want to add a delay at the end of the operation above, we can simple add another deferred.

extern asio::ip::tcp::socket socket1, socket2;
extern asio::steady_timer delay;
char buf[4096];

auto forward_op = 
    socket1.async_read_some(
        asio::buffer(buf), 
        asio::deferred(
            [&](system::error_code ec, std::size_t n)
            {
                return asio::deferred
                    .when(!!ec) // complete with the error and `n`
                    .then(asio::deferred.values(ec, n))
                    .otherwise(
                        asio::async_write(socket2,
                            asio::buffer(buf, n),
                        asio::deferred));
            }
        ))
        (
        asio::deferred(
            [&](system::error_code ec, std::size_t n)
            {
                return asio::deferred
                    .when(!!ec)
                    .then(asio::deferred.values(ec, n))
                    .otherwise(
                        delay.async_wait(asio::append(asio::deferred, n))
                    );
            }
            )
        );

This now gives us a simple composed operation with three steps. It also gets increasingly unreadable, which is why asio provides

operator|

Instead of invoking the deferred expression multiple times, you can also just write this:

extern asio::ip::tcp::socket socket1, socket2;
extern asio::steady_timer delay;
char buf[4096];

auto forward_op = 
    socket1.async_read_some(asio::buffer(buf), asio::deferred) 
        | asio::deferred(
            [&](system::error_code ec, std::size_t n)
            {
                return asio::deferred
                    .when(!!ec) // complete with the error and `n`
                    .then(asio::deferred.values(ec, n))
                    .otherwise(
                        asio::async_write(socket2,
                            asio::buffer(buf, n),
                        asio::deferred));
            }
        )
        | asio::deferred(
            [&](system::error_code ec, std::size_t n)
            {
                return asio::deferred
                    .when(!!ec)
                    .then(asio::deferred.values(ec, n))
                    .otherwise(
                        delay.async_wait(asio::append(asio::deferred, n))
                    );
        });

Readable code

It should be quite clear that the complexity can get out of hand rather quickly, which is why you should consider separating the continuation functions from the deferred chain.

This can be achieved with by using append to pass pointers to the io objects, like so:

auto do_read(asio::ip::tcp::socket * socket1,
             asio::ip::tcp::socket * socket2, 
             char * buf, std::size_t n)
            {
                return socket1->async_read_some(
                    asio::buffer(buf, n), 
                    asio::append(asio::deferred, socket1, buf));
            };

auto do_write(system::error_code ec, std::size_t n,             
              asio::ip::tcp::socket * socket2, char * buf)
            {
                return asio::deferred
                    .when(!!ec) // complete with the error and `n`
                    .then(asio::deferred.values(ec, n))
                    .otherwise(
                        asio::async_write(*socket2,
                            asio::buffer(buf, n),
                        asio::deferred));
            };

template<std::size_t Size>
auto forward_op(
    asio::ip::tcp::socket & socket1,  
    asio::ip::tcp::socket & socket2,
    char (&buf)[Size])
{
    return asio::deferred.values(
        &socket1, &socket2, &buf[0], Size)
        | asio::deferred(&do_read)
        | asio::deferred(&do_write);
}

More examples can be found in the asio repo.

All Posts by This Author