Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Why
CancellationSignal
A
CancellationSignal
can be shared among multiple coroutines, and when a coroutine is started, it can be bound to the signal. The lifetime of the signal will be extended until the registered coroutine has finished running. A signal can only be triggered once.CancellationSignal has such interface:
CancellationSlot
If a coroutine registers a cancellation signal when it starts, it can obtain a
CancellationSlot
for response. When the coroutine is called through co_await, theCancellationSlot
will be automatically propagated. The lifetime of theCancellationSlot
will be extended until the coroutine ends.The propagation of the signal is as follows:
In the above task: If the signal is triggered, then foo(), bar(1) and bar(2) will receive the signal, while bar(0) will not receive the signal.
Users can obtain the current cancellation slot of a coroutine through
co_await currentCancellationSlot{}
; if the coroutine is not bound to a signal, it will returnnullptr
. Thecanceled()
method can be called to check if the current coroutine has been canceled. A signal handling function can be registered using theemplace()
method.The signal handling function will be executed by the thread that initiates the cancel signal, and users need to ensure the thread safety of this function themselves. We guarantee that the signal handling function will be triggered at most once.
The public interface of
CancellationSlot
is as follows. Unlike signals that are shared across multiple coroutines in a thread-safe manner, different coroutines will hold different slots. Therefore, we prohibit concurrent calls to the same slot, as it is not thread-safe.Collect and structured concurrency
The signal and slot mechanism belongs to a low-level API and is generally used for users to implement their own asynchronous components and very complex asynchronous logic. In general code, we recommend using
collectAny
andcollectAll
to implement structured concurrency and cancellation logic. For example, when handling timeouts in the code, we only need to do it like this:The
collectAny
function will internally create a new signal and bind the coroutines in the parameter list to that signal. When the first task finishes, the signal will be triggered.We can set the cancellation signal type for
collectAny
through template parameters.If the coroutine bound to
collectAny
is tied to a cancellation signal, then when the signal is triggered,collectAny
will immediately terminate and throw an exception of typestd::system_error
(error code:std::errc::operation_canceled
). Additionally, the cancellation signal will be forwarded and passed to all coroutines started bycollectAny
.Similarly, collectAll also supports triggering a cancellation signal upon the completion of the first task. The differences between
collectAll
andcollectAny
are as follows:collectAll
waits for all coroutines to finish executing, while collectAny returns immediately after triggering the cancellation signal.collectAll
receives a cancellation signal from the upper layer, it does not throw an exception but will wait for all coroutines to complete their execution. (However, the cancellation signal will still be forwarded to the coroutines started bycollectAll
.)Executor Cancellation
We can try to request the executor to cancel a specific task, but whether the cancellation is successful depends on the implementation of the executor. The schedule interface has been updated to include a parameter, allowing the passing of a
cancellationSlot
, which enables the possibility of canceling tasks that are currently scheduled.Awaitor Cancellation
Cancellation is collaborative, and the correct response to a cancellation signal requires each Awaitor IO object to properly implement cancellation logic. async_simple's Awaitors will throw a std::system_error exception when they are canceled.
The following IO objects exist in async_simple:
CollectAny
/CollectAll
Yield
Sleep
SpinLock
Mutex
SharedMutex
Latch
ConditionVariable
Future
CountingSemaphore
The
Collect*
have implemented cancellation.Yield
,Sleep
, andSpinLock
depend on whether the scheduler implements cancellation (SimpleExecutor
has support cancel sleep). Even if the executor does not support cancellation, Yield and Sleep will insert checkpoints for cancellation during await and resume.uthread
uthread
dont support cancellation now, but The design of signals and slots is independent, so stack coroutines could also support cancellation later. The related design needs to be discussed.What is changing
Breakchanges:
spinlock
may throw exceptions.CollectAll
/CollectAny
have been changed to lazy, and the callback versions ofcollectAll
/collectAny
have been removed.collectAny
will trigger a cancellation signal.TODO
add document & more test later.