Coroutines - Awaiters

Well we are finally ready to go back the my previous post. Here I demonstrate a simple synchronous echo server. As mentioned previously. This can only allow a single client to connect to it at any given time. With coroutines we can do something like:

using namespace std::experimental::coroutines_v1;

async::Task<int> runSession(std::unique_ptr<async::TcpSession> session)
{
    printf("starting read\n");
    //co_await suspends this coroutine until the read completes
    std::string str = co_await session->read(255);
    while (!str.empty()) {
        printf("read: %s\n", str.c_str());
        int strSz = str.size();
        //co_await suspends this coroutine until the write completes
        ssize_t bytesWritten = co_await session->write(std::move(str));
        printf("Written %zd bytes\n", bytesWritten);
        if (bytesWritten < strSz) {
            break;
        }
        str = co_await session->read(255);
    }
    printf("End of file\n");
    co_return 0;
}

async::Task<int> doAccept(std::unique_ptr<async::TcpServer> server)
{
    while (true) {
        printf("Starting accept\n");
        //co_await suspends this coroutine until the write completes
        auto session = co_await server->accept();
        printf("Accept complete\n");
        //move the session into a new co_routine that will operate independently
        //of this one
        auto sessionTask = runSession(std::move(session));
        sessionTask.start();
        sessionTask.detach();
    }
    printf("end of doAccept\n");
    co_return 0;
}

int main()
{
    async::Executor exec;
    auto server = std::make_unique<async::TcpServer>(exec);
    if (server->bind("", 20000)) {
        abort();
    }
   
    if (server->listen()) {
        abort();
    }
    auto task = doAccept(std::move(server));
    task.start();
    exec.run(); 
    return 0;

}

The full example can be found here. The big difference between this example the one here, is instead of just returning the result my socket operations (reads, writes and accepts), I now co_await them.

The co_await operator allows us to suspend our coroutine and return control back to the coroutine caller. This allows us to do other work while waiting our socket operations to complete. When they do complete, we can resume them from exactly where we left off.

The next question then becomes, how do we design our Socket library incorporate co_await functionality.

There are several ways that the co_await operator will process the expression on its right. For now, we will consider the simplest case and that is where our co_await expression returns an Awaiter.

An Awaiter is a simple struct or class that implements the following methods: await_ready, await_suspend and await_resume.

bool await_ready() const {...} simply returns whether we are ready to resume our coroutine or whether we need to look at suspending our coroutine. Assuming await_ready returns false. We proceed to running await_suspend

Several signatures are available for the await_suspend method. The simplest is void await_suspend(coroutine_handle<> handle) {...}. This is the handle for the coroutine object that our co_await will suspend. Once this function completes, control is returned back to caller of the coroutine object. It is this function that is responsible for storing the coroutine handle for later so that our coroutine does not stay suspended forever.

Once handle.resume() is called, await_ready returns false, or some other mechanism resumes our coroutine, we call the method auto await_resume() The return value from await_resume allows the co_await operator to return its value.

Sometimes it is impractical for expr in co_await expr to return an awaiter as described able. If expr returns a class the class may provide its own instance of Awaiter operator co_await (...) which will return the Awaiter. Alternatively one can implement an await_transform` method which will transform expr into an Awaiter.

Now that we have descibed Awaiter, I would like to point out that the initial_suspend and final_suspend methods in our promise_type both return Awaiters. The object suspend_always and suspend_never are trivial awaiters. suspend_always returns true to await_ready and suspend_never returns false. There is nothing stopping you from rolling out your own though.

If you are curious what a real life Awaiter looks like, take a look at my future object. It stores the coroutine handle in a lamda for later processing.