SeniorCodeOccasionalNot answered yet
How do you write a custom Awaitable that resumes a coroutine on I/O completion?
Implement a custom Awaitable ReadAwaitable that suspends the coroutine until an asynchronous I/O read completes, then resumes it and delivers the byte count.
Constraints:
- The coroutine must always suspend to wait for the I/O (never complete synchronously).
- The completion callback must capture the coroutine handle by value, not by reference.
- The result must be stored before the coroutine is resumed.
- The thread must be free to do other work while the I/O is in flight.
#include <coroutine>
struct ReadAwaitable {
IoService& io;
int fd;
ssize_t result = 0;
bool await_ready() const;
void await_suspend(std::coroutine_handle<> h);
ssize_t await_resume() const;
};
Write the implementation.
Make await_ready() return false, have await_suspend(handle) register the I/O and stash the handle in the completion callback, and let await_resume() return the result. The callback calls handle.resume().
- ✗Returning
truefromawait_ready(), so the coroutine never suspends to wait for the I/O - ✗Capturing the
handleby reference in the callback — it dangles onceawait_suspendreturns - ✗Resuming the handle before the result is stored, so
await_resume()reads stale data
- →Why is it safe to resume the handle from the I/O thread rather than the original one?
- →How would you propagate an I/O error out through
await_resume()?
A custom I/O Awaitable
#include <coroutine>
struct ReadAwaitable {
IoService& io;
int fd;
ssize_t result = 0; // lives in the coroutine frame
bool await_ready() const { return false; } // always wait for the I/O
void await_suspend(std::coroutine_handle<> h) {
// Register the operation. The callback captures the handle BY VALUE.
io.async_read(fd, [this, h]() mutable {
result = io.last_bytes(); // store the result first...
h.resume(); // ...then resume the coroutine
});
// await_suspend returns void → control goes back to the caller,
// the thread is free until the I/O completes
}
ssize_t await_resume() const { return result; } // value of the whole co_await
};
Task<void> handle_client(IoService& io, int fd) {
ssize_t n = co_await ReadAwaitable{io, fd}; // suspends until the read finishes
process(n);
}
Key points: result is a member of the Awaitable, which itself lives in the coroutine frame, so it survives the suspension. The callback captures h by value — a coroutine_handle is a lightweight pointer, cheap and safe to copy. The result is written before resume(), otherwise await_resume() would read garbage.