tree: 45f2088c0a8bdf1be738f115168a5f09704d32f3 [path history] [tgz]
  1. context/
  2. BUILD.gn
  3. coroutine.h
  4. coroutine_impl.cc
  5. coroutine_impl.h
  6. coroutine_manager.h
  7. coroutine_manager_unittest.cc
  8. coroutine_unittest.cc
  9. coroutine_waiter.h
  10. coroutine_waiter_unittest.cc
  11. README.md
src/ledger/lib/coroutine/README.md

Coroutine library

Description

This coroutine library can help you write highly asynchronous code, and avoid “callback hell”. It does so by allowing a function (or method) to be paused and resumed by swapping execution contexts. It makes the code look synchronous, while remaining asynchronous under the hood, with all the usual concurrency pitfalls, such as race conditions and deadlocks.

Coroutines are different than multithreading, but both can be used together. See CoroutineHandler for details.

Usage

Here are some tips on good use of the Coroutine library.

Use CoroutineManager in classes

Usually, coroutines created within a class object should not survive its destruction, whether because continuing the processing didn't make sense, or because resources captured by the coroutine would be destroyed (such as this).

CoroutineManager is a proxy class for CoroutineService. CoroutineManager interrupts the coroutine it created when destroyed, and can be created using the CoroutineService vended by an Environment object.

You should consider using CoroutineManager if you use coroutines in your class.

Free-standing functions probably don't need CoroutineManager and can use CoroutineService directly.

When receiving INTERRUPTED, return

coroutine::ContinuationStatus::INTERRUPTED means another part of your code requested the coroutine to terminate gracefully. This would be the case if the CoroutineManager or CoroutineService who created this coroutine are destroyed.

This mechanism is needed because other parts of the program don’t know the heap allocations made inside the coroutine, as well as other cleanup performed by the destructors of objects created or owned by the coroutine. When a coroutine destruction is needed, it is resumed with an ContinuationStatus::INTERRUPTED and it is the coroutine's job to unwind its call stack.

Usually, the only thing you need to do when receiving a ContinuationStatus::INTERRUPTED is to return immediately. Doing more work is dangerous as some objects you rely on may be destroyed already.

Don’t use Yield and Resume

You probably don’t need to use Yield() and Resume() directly. SyncCall is a utility function that can be used to wrap any asynchronous call, so that you don't have to use CoroutineHandler methods directly.

If you have an asynchronous function with the signature AsynchronousCall(Argument, fit::function<void(Status, Result)>), then you can wrap it such as:

Argument argument(...)
Status status;
Result value;
if (coroutine::SyncCall(handler,
    [argument](fit::function<void(Status, Result)> cb) {
        AsynchronousCall(argument, std::move(cb));
    }, &status, &value) == coroutine::ContinuationStatus::INTERRUPTED) {
  return Status::INTERRUPTED;
}
if (status != Status::OK)
  return status;
Process(value);

SyncCall will ensure the asynchronous call is made and the coroutine paused, and then resumed when the asynchronous callback is executed.

Use coroutine::Wait with for loops

Coroutines make it very easy to write asynchronous code, but the execution of the coroutine itself remains sequential. In particular, for loops are not run in parallel. If you can, avoid the following pattern:

std::vector<Result> results;
for (auto& obj : objects_) {
  Result result;
  // Don't do that! SynchronousFrobinate does not need to wait for the previous
  // call to finish.
  if (SynchronousFrobinate(handler, obj, &result) ==
      coroutine::ContinuationStatus::INTERRUPTED) {
    return;
  }
  results.push_back(std::move(result));
}

Instead, make the asynchronous calls directly and use coroutine::Waiter to collate the results:

auto waiter = fxl::MakeRefCounted<
    callback::Waiter<Status, std::unique_ptr<Result>>>(Status::OK);

for (auto& obj : objects_) {
  AsyncFrobinate(obj, waiter->NewCallback());
}

Status status; std::vector<std::unique_ptr<Result>> result;
if (coroutine::Wait(handler, std::move(waiter), &s, &result) ==
    coroutine::ContinuationStatus::INTERRUPTED) {
  return Status::INTERRUPTED;
}