Ответ 1
Здесь есть две проблемы. Во-первых, всегда рекомендуется передать CancellationToken
в API Task.Run
, кроме того, чтобы сделать его доступным для задачи лямбда. Это связывает токен с задачей и имеет жизненно важное значение для правильного распространения отмены, вызванного token.ThrowIfCancellationRequested
.
Это, однако, не объясняет, почему статус отмены для task1
по-прежнему распространяется правильно (task1.Status == TaskStatus.Canceled
), а не для task2
(task2.Status == TaskStatus.Faulted
).
Теперь это может быть один из тех редких случаев, когда умная логика вывода типа С# может играть против разработчика. Здесь подробно обсуждались здесь и здесь. Подводя итог, в случае с task1
, следующий компилятор Task.Run
выводится компилятором:
public static Task Run(Func<Task> function)
а не:
public static Task Run(Action action)
Это потому, что lambda task1
не имеет никакого естественного кода кода из цикла for
, поэтому он может также быть Func<Task>
лямбда, , несмотря на то, что он не async
, и он не вернуть что-либо. Это вариант, который компилятор поддерживает больше, чем Action
. Тогда использование такого переопределения Task.Run
эквивалентно такому:
var task1 = Task.Factory.StartNew(new Func<Task>(() =>
{
for (var i = 0; ; i++)
{
Thread.Sleep(i); // simulate work item #i
token.ThrowIfCancellationRequested();
}
})).Unwrap();
Вложенная задача типа Task<Task>
возвращается Task.Factory.StartNew
, которая получает развернутый до Task
на Unwrap()
. Task.Run
достаточно умный, чтобы сделать такую развертку автоматически, когда он принимает Func<Task>
. Развернутая задача в стиле обещания корректно распространяет статус отмены из своей внутренней задачи, выбрасываемой как исключение OperationCanceledException
с помощью Func<Task>
лямбда. Этого не происходит для task2
, который принимает Action
лямбда и не создает никаких внутренних задач. Отмена не распространяется на task2
, потому что token
не был связан с task2
через Task.Run
.
В конце концов, это может быть желательным поведением для task1
(конечно, не для task2
), но мы не хотим создавать вложенные задачи позади сцены в любом случае. Более того, это поведение для task1
может быть легко нарушено путем введения условного break
из цикла for
.
Правильный код для task1
должен быть указан:
var task1 = Task.Run(new Action(() =>
{
for (var i = 0; ; i++)
{
Thread.Sleep(i); // simulate work item #i
token.ThrowIfCancellationRequested();
}
}), token);