Home > Enterprise >  Awaiting a predicate with C 20 coroutines
Awaiting a predicate with C 20 coroutines

Time:11-27

We started using the modern C 20 coroutines on our project recently. There is a list of coroutines referred to as Tasks in the Executor, which steps through them one by one resuming them. All of this is done on a single thread. Sometimes coroutines need not to be resumed until some predicate is satisfied. In some cases it may be satisfied by another coroutine, which makes suspending for later execution just fine. Here are the types in use:

struct Task : std::coroutine_handle<task_promise_t> {
  using promise_type = task_promise_t;
};
struct task_promise_t {
  Task get_return_object() { return {Task::from_promise(*this)}; }
  std::suspend_always initial_suspend() noexcept { return {}; }
  std::suspend_always final_suspend() noexcept { return {}; }

  void return_void() {}
  void unhandled_exception() {}
};
struct Executor {
  /* snip */

  void enqueue_task(Task &&task) { tasks.push_back(task); }

  void tick() {
    while (!tasks.empty())
      this->step();
  }

  void step() {
    Task task = std::move(tasks.front());
    tasks.pop_front();
    task.resume();
    if (!task.done())
      tasks.push_back(task);
  }

  std::deque<Task> tasks;

  /* snip */
}

Example of how I expect it to be used:

auto exec = Executor();

static bool global_predicate = false;

exec.enqueue_task([](Executor* exec) -> Task {
  co_await WaitFor(/* bool(void) */ []() -> bool { return global_predicate; });
  /* prerequisite satisfied, other logic goes here */
  std::cout << "Hello, world" << std::endl;
}(&exec)); 
exec.step(); // no output, predicate false
exec.step(); // no output, predicate false
global_predicate = true;
exec.step(); // predicate true, "Hello, world!", coroutine is also done 

I did manage to get the implementation going, this seems to work fine.

static bool example_global_predicate;
auto coro = []() -> Task {
  while (!example_global_predicate)
    co_await std::suspend_always();
  /* example_global_predicate is now true, do stuff */
  co_return;
}();

But I can't a good way to generalize and abstract it into it's own class. How would one go about it? I would expect to see that functionality in the standard library, but seeing how customizable the coroutines are I doubt there is a way to implement a one-size-fits-all solution.

CodePudding user response:

The "await" style of coroutines is intended for doing asynchronous processing in a way that mirrors the synchronous equivalent. In sinchronous code, you might write:

int func(float f)
{
   auto value = compute_stuff(f);
   auto val2 = compute_more_stuff(value, 23);
   return val2   value;
}

If one or both of these functions is asychronous, you would rewrite it as follows (assuming the presence of appropriate co_await machinery):

task<int> func(float f)
{
   auto value = compute_stuff(f);
   auto val2 = co_await async_compute_more_stuff(value, 23);
   co_return val2   value;
}

It's structurally the same code except that in one case, func will halt halfway through until async_compute_more_stuff has finished its computation, then be resumed and return its value through Task<int>. The async nature of the code is as implicit as possible; it largely looks like synchronous code.

If you already have some extant async process, and you just want a function to get called when that process concludes, and there is no direct relationship between them, you don't need a coroutine. This code:

static atomic<bool> example_global_predicate;
auto coro = []() -> Task {
  while (!example_global_predicate)
    co_await std::suspend_always();
  /* example_global_predicate is now true, do stuff */
  co_return;
}();

Is not meaningfully different from this:

static atomic<bool> example_global_predicate;

register_polling_task([]() -> bool
{
  if(!example_global_predicate)
     return false;
  /* example_global_predicate is now true, do stuff */
  return true;
});

register_polling_task represents some global construct which will at regular intervals call your function until it returns true, at which point it assumes that it has done its job and removes the task. Your coroutine version might hide this global construct, but it still needs to be there because somebody has to wake the coroutine up.

Overall, this is not an async circumstance where using coroutines buys you anything in particular.

However, it could still be theoretically useful to attach coroutine resumption to a polling task. The most reasonable way to do this is to put the polling in a task outside of a coroutine. That is, coroutines shouldn't poll for the global state; that's someone else's job.

A coroutine would do something like co_await PollingTask(). This hands the coroutine_handle off to the system that polls the global state. When that global state enters the correct state, it will resume that handle. And when executing the co_await expression, it should also check the state then, so that if the state is already signaled, it should just not halt the coroutine's execution.

PollingTask() would return an awaitable that has all of this machinery built into it.

  • Related