Zpracování výjimek úloh

Použijte await jako výchozí. await poskytuje přirozený tok výjimek, udržuje kód čitelný a zabraňuje deadlockům vzniklým při synchronizaci přes asynchronní operace.

Někdy stále potřebujete blokovat na objektu Task, například ve starších synchronních vstupních bodech. V těchto případech je potřeba pochopit, jak jednotlivá rozhraní API odhalí výjimky.

Porovnání šíření výjimek v případě blokujících API rozhraní

Pokud je nutné blokovat úkol, použijte GetAwaiter(). GetResult() pro zachování původního typu výjimky:

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 a Wait obalují výjimky v AggregateException, což komplikuje zpracování výjimek. Následující kód používá tato rozhraní API a obdrží nesprávný typ výjimky:

// ⚠️ 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

U úloh, které mají chybu s více výjimkami, GetAwaiter().GetResult() přesto vyvolá jednu výjimku, ale Task.Exception uloží AggregateException výjimku, která obsahuje všechny vnitřní výjimky:

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 versus GetAwaiter().GetResult()

Tyto pokyny použijte, když zvolíte mezi těmito dvěma rozhraními API:

  • Upřednostňujte await, když je to možné. Vyhne se blokování a riziku uvíznutí.
  • Pokud je nutné blokovat a chcete původní typy výjimek, použijte GetAwaiter().GetResult(). Ve WinForms aplikacích si všimněte sekce „Běžné chyby a zablokování“ v článku o obslužných rutinách událostí.
  • Pokud váš existující kód očekává AggregateException, použijte Result nebo Wait() zkontrolujte InnerExceptions.

Tato pravidla ovlivňují pouze obrazec výjimky. Obě rozhraní API stále blokují aktuální vlákno, takže obě můžou zablokovat jednovláknová SynchronizationContext prostředí. Informace o tom, jak správně dokončit úkoly na všech cestách kódu, najdete v tématu Dokončení úkolů.

Výjimky nepozorovaných úloh v moderním .NET

Modul runtime vyvolá TaskScheduler.UnobservedTaskException, když je ukončen chybný Task dříve, než kód zjistí jeho výjimku.

V moderních verzích .NET už nedochází k tomu, že by nezachycené výjimky ve výchozím nastavení způsobily pád procesu. Modul runtime je hlásí prostřednictvím události a pak pokračuje v provádění.

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

Událost použijte pro diagnostiku a telemetrii. Nepoužívejte událost jako náhradu za normální zpracování výjimek v asynchronních tocích.

Viz také