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.