Delays and Timeouts
Corosio schedules delays and deadlines through two free functions,
delay() and timeout(). Neither one names an I/O object: there is
no timer to construct, store as a member, move around, or close.
Each call returns an awaitable that arms whatever clock resource it
needs for the duration of a single co_await, then releases it. A loop
that waits repeatedly, like a heartbeat or a retry backoff, just calls
delay() again on the next iteration. There is no long-lived object
whose expiry must be reset.
|
Code snippets assume:
|
Delaying for a Duration
delay(duration) suspends the calling coroutine until the given
duration elapses:
auto [ec] = co_await corosio::delay(500ms);
if (!ec)
std::cout << "500ms elapsed\n";
A zero or negative duration completes synchronously without suspending.
Delaying Until a Time Point
delay(time_point) suspends until an absolute steady_clock time is
reached. This is the right tool for periodic work, since it avoids
the drift that accumulates when each iteration re-measures a relative
duration from "now":
auto next = std::chrono::steady_clock::now();
for (int i = 0; i < 10; ++i)
{
next += 100ms;
auto [ec] = co_await corosio::delay(next);
if (ec)
break;
std::cout << "Tick " << i << "\n";
}
A time point already in the past also completes synchronously.
Delaying on a Different Clock
delay(time_point) also accepts a time point on clocks other than
steady_clock. One example is std::chrono::system_clock, for a
deadline naturally expressed as wall-clock time rather than a monotonic
duration:
auto deadline = std::chrono::system_clock::now() + std::chrono::minutes(5);
auto [ec] = co_await corosio::delay(deadline);
Internally this performs one or more bounded steady_clock waits,
re-reading the clock between them. An adjustment to the clock is
therefore observed at the next re-check rather than only at the original
deadline. Pass a custom traits type as delay<Traits>(time_point) to
bound how quickly such an adjustment is observed:
// Re-read the wall clock at least once per second, so a step of
// the clock is observed within that bound
struct capped_traits
{
static std::chrono::system_clock::duration
to_wait_duration(std::chrono::system_clock::duration d)
{
auto cap = std::chrono::system_clock::duration(std::chrono::seconds(1));
return d < cap ? d : cap;
}
};
auto deadline = std::chrono::system_clock::now() + std::chrono::minutes(5);
auto [ec] = co_await corosio::delay<capped_traits>(deadline);
Cancellation
delay() honors the stop token of its co_await environment. If the
token is already stopped when the coroutine suspends, or becomes
stopped while it is waiting, the wait ends early and resumes with
capy::cond::canceled:
auto [ec] = co_await corosio::delay(10s);
if (ec == capy::cond::canceled)
std::cout << "Delay was cancelled\n";
There is no cancel() to call directly; cancellation flows entirely
through the coroutine’s stop token, the same mechanism every other
Corosio operation uses.
Racing an Operation Against a Deadline
timeout(op, duration) starts an awaitable and a deadline together.
Whichever finishes first decides the outcome:
auto [ec, n] = co_await corosio::timeout(sock.read_some(buffer), 200ms);
if (ec == capy::cond::timeout)
std::cout << "No data within 200ms\n";
else if (!ec)
std::cout << "Read " << n << " bytes\n";
If the inner operation wins, its io_result is returned unchanged,
error or success, payload and all. If the deadline wins, the inner
operation is cancelled and timeout() produces its own result. ec is
capy::cond::timeout, and any payload such as a byte count is
default-initialized. It is not whatever the cancelled operation happened
to leave behind. Because the deadline-win path default-initializes
that payload, the inner operation’s io_result payload type must be
default constructible.
timeout() also accepts an absolute deadline:
auto deadline = std::chrono::steady_clock::now() + 5s;
auto [ec] = co_await corosio::timeout(sock.connect(ep), deadline);
Distinguishing Timeout from Cancellation
A timeout() call sits inside whatever stop token its own coroutine
is awaited under. If that parent token is stopped, the race ends the
same way an unguarded operation would. The inner awaitable is cancelled,
and timeout() reports capy::cond::canceled, not
capy::cond::timeout. The two conditions are never ambiguous:
| Result | Meaning |
|---|---|
|
The deadline elapsed before the operation completed. |
|
The coroutine’s own stop token fired, independent of the deadline. |
auto [ec] = co_await corosio::timeout(sock.connect(ep), 3s);
if (ec == capy::cond::timeout)
std::cout << "Connect attempt timed out\n";
else if (ec == capy::cond::canceled)
std::cout << "Connect attempt cancelled by caller\n";
Requires an io_context
Both delay() and timeout() need a clock service to arm, which
they obtain from the awaiting coroutine’s executor. That executor must
belong to an io_context. Awaiting either one from any other kind
of execution context terminates the program: silently skipping the
requested delay would be worse than a hard failure.
Composing with Other Operations
Because timeout() takes any awaitable that yields an io_result,
it composes with anything Corosio returns, not just socket reads. A
connect-with-retry loop pairs timeout() for the per-attempt deadline
with delay() for the pause between attempts:
capy::task<>
connect_with_deadline(
corosio::tcp_socket& sock, corosio::endpoint ep, int max_attempts)
{
for (int attempt = 0; attempt < max_attempts; ++attempt)
{
auto [ec] = co_await corosio::timeout(sock.connect(ep), 3s);
if (!ec)
co_return;
if (ec == capy::cond::canceled)
co_return;
sock.close();
auto [dec] = co_await corosio::delay(500ms);
if (dec == capy::cond::canceled)
co_return;
}
}
Next Steps
-
Signal Handling: respond to OS signals
-
I/O Context: the event loop
-
Error Handling: cancellation patterns