Почему асинхронная задача с исключением успешно завершается?

#c# #asynchronous

Вопрос:

У меня есть консольное приложение на C#, в котором есть ряд асинхронных задач. Одной из задач было создание исключения. Однако я узнал, что код хоста обрабатывал сценарий не так, как ожидалось. Пытаясь понять, что происходит, я решил провести тест. Цель этого теста состояла в том, чтобы создать Task тест, который будет выполняться асинхронно, но намеренно создает исключение. Я хотел посмотреть, соответствует ли то, что было возвращено, ожидаемому кодом хоста.

К моему удивлению, свойство Task объекта IsCompletedSuccessfully было true и IsFaulted свойство было false . Тем не менее, я ожидал IsCompletedSuccessfully , что буду false и IsFaulted буду true . Код выглядит следующим образом:

 [Fact]
public void Repro()
{
  var task = new Task(async () => {
    var operand1 = 2;
    var operand2 = 0;
    var result = operand1 / operand2;
    
    Thread.Sleep(250);
    await Task.CompletedTask;
  });
  
  task.RunSynchronously();
  
  Console.WriteLine($"Cancelled: {task.IsCanceled}, Completed: {task.IsCompleted}, Completed Successfully: {task.IsCompletedSuccessfully}, Faulted: {task.IsFaulted}");
  if (task.Exception != null)
  {
    Console.WriteLine("There was an exception");
  }

  Assert.Equal(false, task.IsCompletedSuccessfully);
  Assert.Equal(true, task.IsFaulted);
}
 

Почему / как успешно выполняется эта задача? И почему это не исправлено? Я ожидал, что он выдаст исключение деления на ноль.

Спасибо.

Комментарии:

1. Похоже, что это меняется в .NET 6: sharplab.io/…

2. Ты создаешь пожар и забываешь о задании. Вместо лямбды создайте метод с такой подписью, как public async Task DivideByZero() , а затем посмотрите, что произойдет.

3. Или просто удалите async и await Task.CompleteTask то и другое из лямбды, и это сработает.

4. Также: никогда, никогда, никогда не используйте Task конструктор.

Ответ №1:

Нет Task конструктора, который принимает a Func<Task> : единственные перегрузки принимают Action or Action<object> .

Поэтому при записи new Task(async () => ...) асинхронная лямбда-код должна компилироваться в async void метод.

Обратите void внимание : нет способа передать Task резервную копию вызывающему объекту, что означает, что Task объект, созданный с new Task(...) помощью, узнает только о том, есть ли исключение, если лямбда бросает его напрямую. Однако, поскольку лямбда async void -это так , она не будет выдавать исключения вызывающему объекту! Тем не менее, он повторно отправит их в пул потоков, что приведет к сбою вашего приложения.

Ответ №2:

Ответ @canton7 отражает поведение, которое вы видите, но они были достаточно любезны, чтобы не говорить то, что я собираюсь сказать, а именно: «вы делаете это неправильно». Смешивание Thread.Sleep с асинхронным/ожиданием, синхронное выполнение асинхронного кода… почему? Просто, почему?

Следующее работает точно так, как ожидалось, потому что оно не пытается делать случайные вещи, которые не имеют смысла:

 [Fact]
public async void _Repro()
{
    var task = Task.Run(async () =>
    {
        var operand1 = 2;
        var operand2 = 0;
        var result = operand1 / operand2;

        await Task.Delay(250);
    });

    await Assert.ThrowsAsync<DivideByZeroException>(async () => await task);

    Console.WriteLine($"Cancelled: {task.IsCanceled}, Completed: {task.IsCompleted}, Completed Successfully: {task.IsCompletedSuccessfully}, Faulted: {task.IsFaulted}");
    if (task.Exception != null)
    {
        Console.WriteLine("There was an exception");
    }

    Assert.False(task.IsCompletedSuccessfully);
    Assert.True(task.IsFaulted);
}
 

Комментарии:

1. Просто чтобы добавить к этому (с чем я полностью согласен): старайтесь не создавать новое Tasks , используя его конструктор. Это старый и в основном устаревший способ ведения дел. Либо используйте Task.Run (или, возможно TaskFactory.StartNew , в крайнем случае), либо используйте async соответствующие методы