The Task.Run
method has overloads that accept both sync and async delegates:
public static Task Run (Action action);
public static Task Run (Func<Task> function);
Unfortunately these overloads don't behave the same when the delegate throws an OperationCanceledException
. The sync delegate results in a Faulted
task, and the async delegate results in a Canceled
task. Here is a minimal demonstration of this behavior:
CancellationToken token = new(canceled: true);
Task taskSync = Task.Run(() => token.ThrowIfCancellationRequested());
Task taskAsync = Task.Run(async () => token.ThrowIfCancellationRequested());
try { Task.WaitAll(taskSync, taskAsync); } catch { }
Console.WriteLine($"taskSync.Status: {taskSync.Status}"); // Faulted
Console.WriteLine($"taskAsync.Status: {taskAsync.Status}"); // Canceled (undesirable)
Output:
taskSync.Status: Faulted
taskAsync.Status: Canceled
This inconsistency has been observed also in this question:
That question asks about the "why". My question here is how to fix it. Specifically my question is: How to implement a variant of the Task.Run
with async delegate, that behaves like the built-in Task.Run
with sync delegate? In case of an OperationCanceledException
it should complete asynchronously as Faulted
, except when the supplied cancellationToken
argument matches the token stored in the exception, in which case it should complete as Canceled
.
public static Task TaskRun2 (Func<Task> action,
CancellationToken cancellationToken = default);
Here is some code with the desirable behavior of the requested method:
CancellationToken token = new(canceled: true);
Task taskA = TaskRun2(async () => token.ThrowIfCancellationRequested());
Task taskB = TaskRun2(async () => token.ThrowIfCancellationRequested(), token);
try { Task.WaitAll(taskA, taskB); } catch { }
Console.WriteLine($"taskA.Status: {taskA.Status}");
Console.WriteLine($"taskB.Status: {taskB.Status}");
Desirable output:
taskA.Status: Faulted
taskB.Status: Canceled