Аналогичный код в задачах, возвращающих разные коды статуса - PullRequest
4 голосов
/ 07 февраля 2020

Я выбрасываю 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 лямбда и async методом, выполняющим один и тот же код.

Ответы [ 2 ]

6 голосов
/ 07 февраля 2020

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

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

Если вы внимательно посмотрите на это 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 и убедитесь, что приложение завершает работу, прежде чем вы сможете нажать клавишу ввода.


Это:

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 переводит Task t2 в состояние Canceled.


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

1 голос
/ 07 февраля 2020

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

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

Об использовании конструктора Task с делегатом asyn c в качестве аргумента, правильный способ сделать это: создание вложенного 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
...