There are two problems here. First, it's always a good idea to pass CancellationToken
to the Task.Run
API, besides making it available to the task's lambda. Doing so associates the token with the task and is vital for the correct propagation of the cancellation triggered by token.ThrowIfCancellationRequested
.
This however doesn't explain why the cancellation status for task1
still gets propagated correctly (task1.Status == TaskStatus.Canceled
), while it doesn't for task2
(task2.Status == TaskStatus.Faulted
).
Now, this might be one of those very rare cases where the clever C# type inference logic can play against the developer's will. It's discussed in great details here and here. To sum up, in case with task1
, the following override of Task.Run
is inferred by compiler:
public static Task Run(Func<Task> function)
rather than:
public static Task Run(Action action)
That's because the task1
lambda has no natural code path out of the for
loop, so it may as well be a Func<Task>
lambda, despite it is not async
and it doesn't return anything. This is the option that compiler favors more than Action
. Then, the use of such override of Task.Run
is equivalent to this:
var task1 = Task.Factory.StartNew(new Func<Task>(() =>
{
for (var i = 0; ; i++)
{
Thread.Sleep(i); // simulate work item #i
token.ThrowIfCancellationRequested();
}
})).Unwrap();
A nested task of type Task<Task>
is returned by Task.Factory.StartNew
, which gets unwrapped to Task
by Unwrap()
. Task.Run
is smart enough to do such unwrapping automatically for when it accepts Func<Task>
. The unwrapped promise-style task correctly propagates the cancellation status from its inner task, thrown as an OperationCanceledException
exception by the Func<Task>
lambda. This doesn't happen for task2
, which accepts an Action
lambda and doesn't create any inner tasks. The cancellation doesn't get propagated for task2
, because token
has not been associated with task2
via Task.Run
.
In the end, this may be a desired behavior for task1
(certainly not for task2
), but we don't want to create nested tasks behind the scene in either case. Moreover, this behavior for task1
may easily get broken by introducing a conditional break
out of the for
loop.
The correct code for task1
should be this:
var task1 = Task.Run(new Action(() =>
{
for (var i = 0; ; i++)
{
Thread.Sleep(i); // simulate work item #i
token.ThrowIfCancellationRequested();
}
}), token);