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.
Here are some tips on good use of the Coroutine library.
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.
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.
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.
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; }