Я добавляю OperationCanceledException из трех разных задач, каждая из которых имеет небольшие отличия, как показано в коде ниже:

static async Task ThrowCancellationException()
{
    throw new OperationCanceledException();
}

static void Main(string[] args)
{
    var t1 = new Task(() => throw new OperationCanceledException());
    t1.Start();
    try { t1.Wait(); } catch { }

    Task t2 = new Task(async () => throw new OperationCanceledException());
    t2.Start();
    try { t2.Wait(); } catch { }

    Task t3 = ThrowCancellationException();

    Console.WriteLine(t1.Status); // prints Faulted
    Console.WriteLine(t2.Status); // prints RanToCompletion
    Console.WriteLine(t3.Status); // prints Canceled
}

Мой вопрос:

Почему статусы разные для каждой задачи?

Я мог понять, что существуют различия между кодом / лямбдами, помеченными async, и лямбдами, не помеченными async, но состояние отличается даже между async лямбдами и {{X3} } метод, выполняющий тот же код.

4
meJustAndrew 7 Фев 2020 в 15:16

2 ответа

Лучший ответ

Я мог бы понять, что существуют различия между кодом / лямбда-кодами, помеченными async, и лямбда-выражениями, не помеченными async, но состояние отличается даже между асинхронным лямбда-кодом и асинхронным методом, выполняющим один и тот же код.

Это не совсем так.

Если вы внимательно посмотрите на new Task(async () => throw new OperationCanceledException()), вы увидите, что он вызывает перегрузку new Task(Action action) (нет перегрузки, которая требует Func<Task>). Это означает, что это эквивалентно передаче метода async void, а не метода async Task.


Так:

Task t2 = new Task(async () => throw new OperationCanceledException());
t2.Start();
try { t2.Wait(); } catch { }

Это компилируется в нечто вроде:

private static async void CompilerGeneratedMethod()
{
    throw new OperationCanceledException()
}
...
Task t2 = new Task(CompilerGeneratedMethod);
t2.Start();
try { t2.Wait(); } catch { }

Это извлекает поток из ThreadPool и запускает CompilerGeneratedMethod на нем. Когда исключение выбрасывается изнутри метода async void, исключение повторно генерируется где-то уместно (в этом случае оно повторно генерируется в ThreadPool), но сам метод CompilerGeneratedMethod сразу возвращается. Это приводит к немедленному завершению Task t2, поэтому его статус RanToCompletion.

Так что же случилось с исключением? Это собирается закрыть вашу заявку! Вставьте Console.ReadLine в конце своего Main и убедитесь, что приложение завершает работу, прежде чем вы сможете нажать Enter.


Этот:

Task t3 = ThrowCancellationException();

Очень разные. Он не пытается ничего запустить на ThreadPool. ThrowCancellationException выполняется синхронно и синхронно возвращает Task, который содержит OperationCanceledException. Task, который содержит OperationCanceledException, рассматривается как Canceled.


Если вы хотите запустить метод async в ThreadPool, используйте Task.Run. Это имеет перегрузку, которая принимает Func<Task>, что означает, что:

Task t2 = Task.Run(async () => throw new OperationCanceledException());

Компилируется что-то вроде:

private static async Task CompilerGeneratedMethod()
{
    throw new OperationCanceledException();
}
...
Task t2 = Task.Run(CompilerGeneratedMethod);

Здесь, когда CompilerGeneratedMethod выполняется в ThreadPool, он возвращает Task, содержащий OperationCanceledException. Затем механизм задачи переводит Task t2 в состояние Canceled.


Кроме того, избегайте new Task и предпочитайте использовать Task.Run, если вы хотите явно запустить метод в ThreadPool. В TPL есть много методов, которые были введены до async / await и которые путают с ним.

6
canton7 7 Фев 2020 в 12:35

Когда вы создаете задачу с помощью конструктора Task, вы можете предоставить CancellationToken в качестве необязательного аргумента. Задача приведет к состоянию Canceled только в случае OperationCanceledException, который связан с этим конкретным CancellationToken. В противном случае исключение будет интерпретировано как ошибка. Вот как вы можете связать OperationCanceledException с CancellationToken:

var cts = new CancellationTokenSource();
var t1 = new Task(() =>
{
    cts.Cancel();
    throw new OperationCanceledException(cts.Token);
}, cts.Token);

Чтобы использовать конструктор Task с асинхронным делегатом в качестве аргумента, правильный способ сделать это - создать вложенный Task<Task>:

Task<Task> t2 = new Task<Task>(async () => throw new OperationCanceledException());
t2.Start();
try { t2.Result.Wait(); } catch { }

Console.WriteLine(t2.Result.Status); // prints Canceled
1
Theodor Zoulias 7 Фев 2020 в 12:51