The following code simulates short bursts of work by sleeping 1ms at a time in order to achieve a cancellable task that takes a total of 2s. That work is then launched in three different ways.
- The first
co_spawn(labelled Task-1) explicitly usesbind_cancellation_slotand successfully cancels the work after 1s. - The second
co_spawnusesawaitable_operatorsandsteady_timer::async_waitto deliver the cancellation after 200ms, but that attempt fails, resulting in the full 2s execution duration. - The third
co_spawnuses a variation of the work that explicitly usesboost::asio::deferbefore starting the work to resolve the issue with the previous attempt.
Is the third attempt at using co_spawn correct? It seems wrong to me that my task's implementation needs to defensively defer at the very beginning in order to work with the awaitable_operators. Is there a better way to implement cancellation of such a long running task?
As a sidenote: I am aware that using co_await on an asynchronous task inside SyncWork would enable cancellation at suspension points. But I really am looking for a way to be more proactive with cancellation in between suspension points. I believe this issue is mostly relevant to the period of time when the coroutine starts and before the first suspension point is eventually hit.
#include <boost/asio.hpp>
#include <boost/asio/experimental/awaitable_operators.hpp>
#include <iostream>
// Simulate work that lasts for 2s, and that checks for cancellation at every 1ms
boost::asio::awaitable<void>
SyncWork()
{
using namespace std::chrono_literals;
auto before = std::chrono::steady_clock::now();
auto limit = before + 2s;
while (std::chrono::steady_clock::now() < limit) {
auto cs = co_await boost::asio::this_coro::cancellation_state;
if (cs.cancelled() != boost::asio::cancellation_type::none) break;
std::this_thread::sleep_for(1ms);
}
std::cout << "SyncWork - elapsed: " << std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - before).count() << "ms\n";
}
boost::asio::awaitable<void>
DeferredSyncWork()
{
auto executor = co_await boost::asio::this_coro::executor;
co_await boost::asio::defer(executor, boost::asio::use_awaitable);
co_await SyncWork();
}
boost::asio::awaitable<void>
timeout()
{
auto executor = co_await boost::asio::this_coro::executor;
boost::asio::steady_timer timer{executor};
timer.expires_from_now(std::chrono::milliseconds{200});
co_await timer.async_wait(boost::asio::use_awaitable);
std::cout << "timeout\n";
}
boost::asio::awaitable<void>
WorkWithTimeout()
{
using namespace boost::asio::experimental::awaitable_operators;
co_await (SyncWork() || timeout());
}
boost::asio::awaitable<void>
DeferredWorkWithTimeout()
{
using namespace boost::asio::experimental::awaitable_operators;
co_await (DeferredSyncWork() || timeout());
}
void HandleException(std::exception_ptr p)
{
if (p) {
try {
std::rethrow_exception(p);
} catch (const std::exception& e) {
std::cout << e.what() << '\n';
} catch (...) {
std::cout << "catch\n";
}
} else {
std::cout << "success\n";
}
}
int main()
{
boost::asio::thread_pool pool{8};
boost::asio::cancellation_signal signal;
boost::asio::co_spawn(pool, SyncWork(), boost::asio::bind_cancellation_slot(signal.slot(), HandleException)); // Task-1
boost::asio::co_spawn(pool, DeferredWorkWithTimeout(), HandleException); // Task-2
boost::asio::co_spawn(pool, WorkWithTimeout(), HandleException); // Task-3
std::this_thread::sleep_for(std::chrono::seconds{1});
signal.emit(boost::asio::cancellation_type_t::terminal);
pool.join();
}
Here is the program output:
timeout
SyncWork - elapsed: 202ms
success
SyncWork - elapsed: 1005ms
success
SyncWork - elapsed: 2000ms
success
Program ended with exit code: 0