CancellableTask
Cooperative cancellation wrapper for Func<Task>. Tracks the active task per async flow using AsyncLocal<CancellableTask> and hooks into the Yield system's OnBeforeYield/OnAfterYield events to check for cancellation at every yield point. Provides fluent callback registration for begin, complete, cancel, and end lifecycle events.
Definition
Namespace: Paragon.Core.Async Assembly: Paragon.dll
public class CancellableTaskRemarks
CancellableTask is the mechanism that makes async methods cancellable without manual CancellationToken passing. It works through two key mechanisms:
AsyncLocal<CancellableTask>— WhenInvokeAsync()runs, it sets itself as the active task inAsyncLocalstorage. BecauseAsyncLocalflows throughasync/awaitcontinuations, any code downstream can find the active task.Yield hooks — A static constructor subscribes to
Yield.OnBeforeYieldandYield.OnAfterYield. At every yield point (e.g.,await Yield.WaitForUpdate()),ThrowIfCancellationRequested()checks whether the active task'sCancellationTokenSourcehas been cancelled. If so, it throwsOperationCanceledException.
This means any async method using Yield methods is automatically cancellable without any explicit token handling — simply call Cancel() on the CancellableTask, and the next Yield call will throw.
Lifecycle
InvokeAsync() called
└─ onBegin callback
└─ Set as AsyncLocal active task
└─ taskFunction.Invoke()
├─ Success path:
│ └─ onComplete callback
│ └─ onEnd callback
└─ Cancel path (OperationCanceledException):
└─ onCanceled callback
└─ onEnd callbackThe Action base class uses CancellableTask internally to make all actions cancellable.
Quick Lookup
Create a cancellable task
new CancellableTask(async () => { ... })
Run (fire-and-forget)
task.Invoke()
Run (awaitable)
await task.InvokeAsync()
Cancel
task.Cancel()
Check if running
task.IsRunning
Register begin callback
task.OnBegin(() => { ... })
Register complete callback
task.OnComplete(() => { ... })
Register cancel callback
task.OnCanceled(() => { ... })
Register always-run callback
task.OnEnd(() => { ... })
Properties
IsRunning
Returns true while the task function is executing.
Methods
Invoke
Starts execution without returning a Task. Fire-and-forget wrapper around InvokeAsync().
InvokeAsync
Starts execution and returns an awaitable Task. Sets this task as the AsyncLocal active task, invokes the task function, and routes to the appropriate callback based on the outcome.
Assertion: Will assert if called while the task is already running. Always check IsRunning before calling.
Cancel
Requests cancellation by calling Cancel() on the internal CancellationTokenSource. The actual cancellation occurs at the next yield point (i.e., when Yield.WaitForUpdate() or similar is awaited).
Cancellation is cooperative — it does not interrupt the task immediately. The task continues until it hits a Yield method, at which point ThrowIfCancellationRequested() fires.
OnBegin
Registers a callback invoked when execution starts (before the task function). Returns this for fluent chaining.
OnComplete
Registers a callback invoked when the task function completes successfully (no cancellation). Returns this for fluent chaining.
OnCanceled
Registers a callback invoked when the task is cancelled (catches OperationCanceledException). Returns this for fluent chaining.
OnEnd
Registers a callback invoked after both success and cancellation (runs in the finally block). Returns this for fluent chaining.
Implementation Requirements
When using CancellableTask, you MUST:
Use
Yieldmethods (WaitForUpdate(),WaitForSeconds(), etc.) as yield points inside the task function — these are where cancellation is checkedNot call
InvokeAsync()whileIsRunningistrue(assertion fires)
You SHOULD:
Register
OnCanceledto handle cleanup when cancellation occursRegister
OnEndfor cleanup that must happen regardless of outcomeUse the fluent API for clean callback registration
You SHOULD NOT:
Use
Task.Delay()orawait Task.Yield()inside the task function — these bypass the cancellation systemCall
Cancel()beforeInvoke()/InvokeAsync()— theCancellationTokenSourcedoes not exist until execution startsRely on immediate cancellation — there is always a delay until the next yield point
Common Pitfalls
Using Task.Delay() bypasses cancellation Task.Delay() does not trigger Yield.OnBeforeYield / OnAfterYield. Use Yield.WaitForSeconds() instead, which integrates with the cancellation system.
Cancellation is not immediate Cancel() only sets the CancellationTokenSource flag. If the task function has a long synchronous section without any Yield calls, cancellation will be delayed until the next yield point.
Double-invoke assertion Calling Invoke() or InvokeAsync() while IsRunning is true triggers Debug.Assert. Always check IsRunning first, or await the previous invocation.
AsyncLocal scope AsyncLocal<CancellableTask> flows through async/await continuations but does not flow into manually spawned threads (new Thread(...)) or Task.Run(). If you spin up background threads inside a CancellableTask, the cancellation hooks will not be active on those threads.
Examples
Basic Cancellable Task
Fluent Lifecycle Registration
Integration with Action System
See Also
SynchronizationContext System — system overview
ParagonSynchronizationContext — the context that executes continuations
Action — primary consumer of CancellableTask
WorkExecutionTime — frame phases for continuation routing
Last updated