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.