Obsługa wyjątków zadań

Użyj await wartości domyślnej. await Zapewnia naturalny przepływ wyjątków, utrzymuje czytelność kodu i pozwala uniknąć zakleszczeń synchronizacji w asynchronicznych operacjach.

Czasami nadal trzeba zablokować obiekt , Taskna przykład w starszych punktach wejścia synchronicznych. W takich przypadkach należy zrozumieć, jak każdy interfejs API przedstawia wyjątki.

Porównanie propagacji wyjątków dla blokujących interfejsów API

Jeśli musisz zablokować zadanie, użyj polecenia GetAwaiter(). GetResult() w celu zachowania oryginalnego typu wyjątku:

public static class SingleExceptionExample
{
    public static Task<int> FaultAsync()
    {
        return Task.FromException<int>(new InvalidOperationException("Single failure"));
    }

    public static void ShowBlockingDifferences()
    {
        try
        {
            _ = FaultAsync().GetAwaiter().GetResult();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"GetAwaiter().GetResult() threw {ex.GetType().Name}");
        }
    }
}
Public Module SingleExceptionExample
    Public Function FaultAsync() As Task(Of Integer)
        Return Task.FromException(Of Integer)(New InvalidOperationException("Single failure"))
    End Function

    Public Sub ShowBlockingDifferences()
        Try
            Dim ignored = FaultAsync().GetAwaiter().GetResult()
        Catch ex As Exception
            Console.WriteLine($"GetAwaiter().GetResult() threw {ex.GetType().Name}")
        End Try
    End Sub
End Module

Task<TResult>.Result i Wait zawijają wyjątki w AggregateException, co komplikuje obsługę wyjątków. Poniższy kod używa tych interfejsów API i otrzymuje nieprawidłowy typ wyjątku:

// ⚠️ DON'T copy this snippet. It demonstrates a problem where exceptions get wrapped unnecessarily.
public static class SingleExceptionBadExample
{
    public static Task<int> FaultAsync()
    {
        return Task.FromException<int>(new InvalidOperationException("Single failure"));
    }

    public static void ShowBlockingDifferences()
    {
        try
        {
            _ = FaultAsync().Result;
        }
        catch (AggregateException ex)
        {
            Console.WriteLine($".Result threw {ex.GetType().Name} with inner {ex.InnerException?.GetType().Name}");
        }
    }
}
' ⚠️ DON'T copy this snippet. It demonstrates a problem where exceptions get wrapped unnecessarily.
Public Module SingleExceptionBadExample
    Public Function FaultAsync() As Task(Of Integer)
        Return Task.FromException(Of Integer)(New InvalidOperationException("Single failure"))
    End Function

    Public Sub ShowBlockingDifferences()
        Try
            Dim ignored = FaultAsync().Result
        Catch ex As AggregateException
            Console.WriteLine($".Result threw {ex.GetType().Name} with inner {ex.InnerException?.GetType().Name}")
        End Try
    End Sub
End Module

W przypadku zadań, które kończą się niepowodzeniem z powodu wielu wyjątków, GetAwaiter().GetResult() nadal wyrzuca jeden wyjątek, ale Task.Exception przechowuje obiekt AggregateException zawierający wszystkie wyjątki potomne:

public static class MultiExceptionExample
{
    public static async Task FaultAfterDelayAsync(string name, int milliseconds)
    {
        await Task.Delay(milliseconds);
        throw new InvalidOperationException($"{name} failed");
    }

    public static void ShowMultipleExceptions()
    {
        Task combined = Task.WhenAll(
            FaultAfterDelayAsync("First", 10),
            FaultAfterDelayAsync("Second", 20));

        try
        {
            combined.GetAwaiter().GetResult();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"GetAwaiter().GetResult() surfaced: {ex.Message}");
        }

        if (combined.IsFaulted && combined.Exception is not null)
        {
            AggregateException allErrors = combined.Exception.Flatten();
            Console.WriteLine($"Task.Exception contains {allErrors.InnerExceptions.Count} exceptions.");
        }
        else
        {
            Console.WriteLine("Task.Exception is null because the task didn't fault.");
        }
    }
}
Public Module MultiExceptionExample
    Public Async Function FaultAfterDelayAsync(name As String, milliseconds As Integer) As Task
        Await Task.Delay(milliseconds)
        Throw New InvalidOperationException($"{name} failed")
    End Function

    Public Sub ShowMultipleExceptions()
        Dim combined As Task = Task.WhenAll(
            FaultAfterDelayAsync("First", 10),
            FaultAfterDelayAsync("Second", 20))

        Try
            combined.GetAwaiter().GetResult()
        Catch ex As Exception
            Console.WriteLine($"GetAwaiter().GetResult() surfaced: {ex.Message}")
        End Try

        If combined.IsFaulted AndAlso combined.Exception IsNot Nothing Then
            Dim allErrors As AggregateException = combined.Exception.Flatten()
            Console.WriteLine($"Task.Exception contains {allErrors.InnerExceptions.Count} exceptions.")
        Else
            Console.WriteLine("Task.Exception was not available because the task did not fault.")
        End If
    End Sub
End Module

Task.Result Vs GetAwaiter().GetResult()

Skorzystaj z tych wskazówek, wybierając między dwoma interfejsami API:

  • Preferuj await , kiedy możesz. Unika zagrożenia blokadą i ryzyka zakleszczenia.
  • Jeśli musisz blokować i chcesz zachować oryginalne typy wyjątków, użyj GetAwaiter().GetResult(). W aplikacjach WinForms zanotuj sekcję Typowe pułapki i zakleszczenia w artykule dotyczącym procedur obsługi zdarzeń.
  • Jeśli Twój istniejący kod oczekuje AggregateException, użyj Result lub Wait() i sprawdź InnerExceptions.

Te reguły mają wpływ tylko na kształt wyjątku. Oba interfejsy API nadal blokują bieżący wątek, więc oba mogą powodować blokadę w środowiskach jednowątkowych SynchronizationContext. Aby dowiedzieć się, jak prawidłowo wykonywać zadania we wszystkich ścieżkach kodu, zobacz Wykonywanie zadań.

Nieobserwowane wyjątki zadań w .NET

Środowisko uruchomieniowe zgłasza TaskScheduler.UnobservedTaskException w momencie, gdy uszkodzony Task jest sfinalizowany przed tym, jak kod zauważy jego wyjątek.

W nowoczesnym .NET nieobserwowane wyjątki nie powodują już domyślnie zakończenia procesu. Środowisko uruchomieniowe zgłasza je za pośrednictwem zdarzenia, a następnie kontynuuje wykonywanie.

public static class UnobservedTaskExceptionExample
{
    public static void ShowEventBehavior()
    {
        bool eventRaised = false;

        TaskScheduler.UnobservedTaskException += (_, args) =>
        {
            eventRaised = true;
            Console.WriteLine($"UnobservedTaskException raised with {args.Exception.InnerExceptions.Count} exception(s).");
            args.SetObserved();
        };

        _ = Task.Run(() => throw new ApplicationException("Background failure"));

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        Console.WriteLine(eventRaised
            ? "Event was raised. The process continued."
            : "Event was not observed in this short run. The process still continued.");
    }
}
Public Module UnobservedTaskExceptionExample
    Public Sub ShowEventBehavior()
        Dim eventRaised As Boolean = False

        AddHandler TaskScheduler.UnobservedTaskException,
            Sub(sender, args)
                eventRaised = True
                Console.WriteLine($"UnobservedTaskException raised with {args.Exception.InnerExceptions.Count} exception(s).")
                args.SetObserved()
            End Sub

        Task.Run(Sub() Throw New ApplicationException("Background failure"))

        GC.Collect()
        GC.WaitForPendingFinalizers()
        GC.Collect()

        If eventRaised Then
            Console.WriteLine("Event was raised. The process continued.")
        Else
            Console.WriteLine("Event was not observed in this short run. The process still continued.")
        End If
    End Sub
End Module

Użyj zdarzenia do diagnostyki i telemetrii. Nie należy używać zdarzenia jako zamiennika dla normalnej obsługi wyjątków w przepływach asynchronicznych.

Zobacz także