Przeczytaj w języku angielskim

Udostępnij za pośrednictwem


Obsługa wyjątków (biblioteka równoległa zadań)

Nieobsługiwane wyjątki zgłaszane przez kod użytkownika uruchomiony wewnątrz zadania są propagowane z powrotem do wątku wywołującego, z wyjątkiem niektórych scenariuszy opisanych w dalszej części tego tematu. Wyjątki są propagowane podczas korzystania z jednej z metod statycznych lub instancji Task.Wait, i są obsługiwane przez dołączenie wywołania w instrukcję /trycatch. Jeśli zadanie jest elementem nadrzędnym dołączonych zadań podrzędnych lub jeśli oczekujesz na wiele zadań, może zostać zgłoszonych wiele wyjątków.

Aby propagować wszystkie wyjątki z powrotem do wątku wywołującego, infrastruktura zadań opakowuje je w instancji AggregateException. Wyjątek AggregateException ma właściwość InnerExceptions, którą można wyliczyć w celu zbadania wszystkich zgłoszonych oryginalnych wyjątków i obsługiwać (lub nie obsługiwać) każdego z osobna. Można również obsługiwać oryginalne wyjątki przy użyciu AggregateException.Handle metody .

Nawet jeśli zgłaszany jest tylko jeden wyjątek, nadal jest opakowany w AggregateException wyjątek, co pokazuje poniższy przykład.


public static partial class Program
{
    public static void HandleThree()
    {
        var task = Task.Run(
            () => throw new CustomException("This exception is expected!"));

        try
        {
            task.Wait();
        }
        catch (AggregateException ae)
        {
            foreach (var ex in ae.InnerExceptions)
            {
                // Handle the custom exception.
                if (ex is CustomException)
                {
                    Console.WriteLine(ex.Message);
                }
                // Rethrow any other exception.
                else
                {
                    throw ex;
                }
            }
        }
    }
}
// The example displays the following output:
//        This exception is expected!

Można uniknąć nieobsługiwanego wyjątku, przechwytując element AggregateException i nie obserwując żadnego z wyjątków wewnętrznych. Zalecamy jednak, aby tego nie robić, ponieważ jest to analogiczne do przechwytywania typu podstawowego Exception w scenariuszach nierównoległych. Przechwycenie wyjątku bez podejmowania specyficznych działań naprawczych może spowodować, że program znajdzie się w nieokreślonym stanie.

Jeśli nie chcesz wywoływać metody Task.Wait w celu oczekiwania na ukończenie zadania, możesz również pobrać wyjątek AggregateException z zadania właściwości Exception, jak pokazano w poniższym przykładzie. Aby uzyskać więcej informacji, zobacz sekcję Obserwowanie wyjątków przy użyciu właściwości Task.Exception w tym temacie.


public static partial class Program
{
    public static void HandleFour()
    {
        var task = Task.Run(
            () => throw new CustomException("This exception is expected!"));

        while (!task.IsCompleted) { }

        if (task.Status == TaskStatus.Faulted)
        {
            foreach (var ex in task.Exception?.InnerExceptions ?? new(Array.Empty<Exception>()))
            {
                // Handle the custom exception.
                if (ex is CustomException)
                {
                    Console.WriteLine(ex.Message);
                }
                // Rethrow any other exception.
                else
                {
                    throw ex;
                }
            }
        }
    }
}
// The example displays the following output:
//        This exception is expected!

Przestroga

Powyższy przykładowy kod zawiera pętlę while , która sonduje właściwość zadania Task.IsCompleted w celu określenia, kiedy zadanie zostało ukończone. Nie należy tego robić w kodzie produkcyjnym, ponieważ jest bardzo nieefektywny.

Jeśli nie zaczekasz na zadanie, które propaguje wyjątek, lub uzyskasz dostęp do jego właściwości Exception, wyjątek jest eskalowany zgodnie z polityką wyjątków platformy .NET, gdy zadanie zostanie poddane odśmiecaniu.

Jeśli wyjątki mogą przechodzić z powrotem do wątku, do którego przyłączamy, może się zdarzyć, że zadanie nadal przetwarza niektóre elementy po wystąpieniu wyjątku.

Uwaga

Po włączeniu opcji "Tylko mój kod" program Visual Studio w niektórych przypadkach zatrzyma się na linii, która zgłasza wyjątek, i wyświetli komunikat o błędzie informujący, że "wyjątek nie jest obsługiwany przez kod użytkownika". Ten błąd jest nieszkodliwy. Możesz nacisnąć F5, aby kontynuować i zobaczyć zachowanie obsługi wyjątków, które przedstawiono w tych przykładach. Aby zapobiec zatrzymywaniu programu Visual Studio na pierwszym błędzie, usuń zaznaczenie pola wyboru Włącz tylko mój kod w sekcji Narzędzia, Opcje, Debugowanie, Ogólne.

Dołączone zadania podrzędne i zagnieżdżone AggregateExceptions

Jeśli zadanie ma dołączone zadanie podrzędne, które zgłasza wyjątek, ten wyjątek jest owinięty w AggregateException przed jego propagacją do zadania nadrzędnego, które owija ten wyjątek we własne AggregateException zanim zostanie przekazany z powrotem do wątku wywołującego. W takich przypadkach właściwość InnerExceptions wyjątku AggregateException przechwyconego w metodzie Task.Wait, WaitAny lub WaitAll zawiera jedno lub więcej AggregateException wystąpień, a nie oryginalne wyjątki, które spowodowały błąd. Aby uniknąć konieczności iterowania zagnieżdżonych AggregateException wyjątków, możesz użyć metody Flatten , aby usunąć wszystkie wyjątki zagnieżdżone AggregateException, tak aby właściwość AggregateException.InnerExceptions zawierała oryginalne wyjątki. W poniższym przykładzie zagnieżdżone wystąpienia AggregateException są spłaszczane i przetwarzane w jednej pętli.


public static partial class Program
{
    public static void FlattenTwo()
    {
        var task = Task.Factory.StartNew(() =>
        {
            var child = Task.Factory.StartNew(() =>
            {
                var grandChild = Task.Factory.StartNew(() =>
                {
                    // This exception is nested inside three AggregateExceptions.
                    throw new CustomException("Attached child2 faulted.");
                }, TaskCreationOptions.AttachedToParent);

                // This exception is nested inside two AggregateExceptions.
                throw new CustomException("Attached child1 faulted.");
            }, TaskCreationOptions.AttachedToParent);
        });

        try
        {
            task.Wait();
        }
        catch (AggregateException ae)
        {
            foreach (var ex in ae.Flatten().InnerExceptions)
            {
                if (ex is CustomException)
                {
                    Console.WriteLine(ex.Message);
                }
                else
                {
                    throw;
                }
            }
        }
    }
}
// The example displays the following output:
//    Attached child1 faulted.
//    Attached child2 faulted.

Można również użyć metody AggregateException.Flatten, aby ponownie zgłosić wyjątki wewnętrzne z wielu wystąpień AggregateException rozrzuconych przez wiele zadań w jednym wystąpieniu AggregateException, jak pokazano w poniższym przykładzie.

public static partial class Program
{
    public static void TaskExceptionTwo()
    {
        try
        {
            ExecuteTasks();
        }
        catch (AggregateException ae)
        {
            foreach (var e in ae.InnerExceptions)
            {
                Console.WriteLine($"{e.GetType().Name}:\n   {e.Message}");
            }
        }
    }

    static void ExecuteTasks()
    {
        // Assume this is a user-entered String.
        string path = @"C:\";
        List<Task> tasks = new();

        tasks.Add(Task.Run(() =>
        {
            // This should throw an UnauthorizedAccessException.
            return Directory.GetFiles(
                path, "*.txt",
                SearchOption.AllDirectories);
        }));

        tasks.Add(Task.Run(() =>
        {
            if (path == @"C:\")
            {
                throw new ArgumentException(
                    "The system root is not a valid path.");
            }
            return new string[] { ".txt", ".dll", ".exe", ".bin", ".dat" };
        }));

        tasks.Add(Task.Run(() =>
        {
            throw new NotImplementedException(
                "This operation has not been implemented.");
        }));

        try
        {
            Task.WaitAll(tasks.ToArray());
        }
        catch (AggregateException ae)
        {
            throw ae.Flatten();
        }
    }
}
// The example displays the following output:
//       UnauthorizedAccessException:
//          Access to the path 'C:\Documents and Settings' is denied.
//       ArgumentException:
//          The system root is not a valid path.
//       NotImplementedException:
//          This operation has not been implemented.

Wyjątki od odłączonych zadań podrzędnych

Domyślnie zadania podrzędne są tworzone jako odłączone. Wyjątki zgłaszane z zadań odłączonych muszą być obsługiwane lub ponownie zgłaszane w bezpośrednim zadaniu nadrzędnym; nie są one przekazywane z powrotem do wątku wywołującego w taki sam sposób, jak to jest w przypadku, gdy dołączone zadania podrzędne są przekazywane z powrotem. Najwyższy element nadrzędny może ręcznie ponownie zgłosić wyjątek z odłączonego elementu podrzędnego, aby spowodować jego opakowanie i AggregateException propagowanie z powrotem do wątku wywołującego.


public static partial class Program
{
    public static void DetachedTwo()
    {
        var task = Task.Run(() =>
        {
            var nestedTask = Task.Run(
                () => throw new CustomException("Detached child task faulted."));

            // Here the exception will be escalated back to the calling thread.
            // We could use try/catch here to prevent that.
            nestedTask.Wait();
        });

        try
        {
            task.Wait();
        }
        catch (AggregateException ae)
        {
            foreach (var e in ae.Flatten().InnerExceptions)
            {
                if (e is CustomException)
                {
                    Console.WriteLine(e.Message);
                }
            }
        }
    }
}
// The example displays the following output:
//    Detached child task faulted.

Nawet jeśli używasz kontynuacji do obserwowania wyjątku w zadaniu podrzędnym, wyjątek nadal musi być obserwowany przez zadanie nadrzędne.

Wyjątki wskazujące anulowanie współpracy

Kiedy kod użytkownika w zadaniu odpowiada na żądanie anulowania, prawidłową procedurą jest wyrzucenie wyjątku OperationCanceledException z przekazaniem tokenu anulowania, na którym oparto to żądanie. Przed próbą propagacji wyjątku wystąpienie zadania porównuje token w wyjątku z tym, który został przekazany do niego podczas jego tworzenia. Jeśli są one takie same, zadanie propaguje TaskCanceledException zawinięte w AggregateException, i można je zobaczyć, gdy zostaną zbadane wyjątki wewnętrzne. Jeśli jednak wątek wywołujący nie czeka na zadanie, ten konkretny wyjątek nie zostanie rozpropagowany. Aby uzyskać więcej informacji, zobacz Anulowanie zadania.

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var task = Task.Factory.StartNew(() =>
{
    CancellationToken ct = token;
    while (someCondition)
    {
        // Do some work...
        Thread.SpinWait(50_000);
        ct.ThrowIfCancellationRequested();
    }
},
token);

// No waiting required.
tokenSource.Dispose();

Używanie metody handle do filtrowania wyjątków wewnętrznych

Możesz użyć AggregateException.Handle metody , aby odfiltrować wyjątki, które można traktować jako "obsługiwane" bez użycia dalszej logiki. W delegacie użytkownika dostarczonym do metody AggregateException.Handle(Func<Exception,Boolean>), można sprawdzić typ wyjątku, jego właściwość Message lub inne informacje, które pozwolą określić, czy jest nieszkodliwy. Wszelkie wyjątki, dla których delegat zwraca false, są rzucane ponownie w nowym wystąpieniu AggregateException natychmiast po zakończeniu działania metody AggregateException.Handle.

Poniższy przykład jest funkcjonalnie odpowiednikiem pierwszego przykładu w tym temacie, który analizuje każdy wyjątek w kolekcji AggregateException.InnerExceptions. Zamiast tego ta procedura obsługi wyjątków wywołuje AggregateException.Handle obiekt metody dla każdego wyjątku i ponownie rzuca tylko te wyjątki, które nie są wystąpieniami CustomException.


public static partial class Program
{
    public static void HandleMethodThree()
    {
        var task = Task.Run(
            () => throw new CustomException("This exception is expected!"));

        try
        {
            task.Wait();
        }
        catch (AggregateException ae)
        {
            // Call the Handle method to handle the custom exception,
            // otherwise rethrow the exception.
            ae.Handle(ex =>
            {
                if (ex is CustomException)
                {
                    Console.WriteLine(ex.Message);
                }
                return ex is CustomException;
            });
        }
    }
}
// The example displays the following output:
//        This exception is expected!

Poniżej przedstawiono bardziej kompletny przykład, który używa metody AggregateException.Handle w celu zapewnienia obsługi specjalnej dla wyjątku UnauthorizedAccessException podczas wyliczania plików.

public static partial class Program
{
    public static void TaskException()
    {
        // This should throw an UnauthorizedAccessException.
        try
        {
            if (GetAllFiles(@"C:\") is { Length: > 0 } files)
            {
                foreach (var file in files)
                {
                    Console.WriteLine(file);
                }
            }
        }
        catch (AggregateException ae)
        {
            foreach (var ex in ae.InnerExceptions)
            {
                Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
            }
        }
        Console.WriteLine();

        // This should throw an ArgumentException.
        try
        {
            foreach (var s in GetAllFiles(""))
            {
                Console.WriteLine(s);
            }
        }
        catch (AggregateException ae)
        {
            foreach (var ex in ae.InnerExceptions)
                Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
        }
    }

    static string[] GetAllFiles(string path)
    {
        var task1 =
            Task.Run(() => Directory.GetFiles(
                path, "*.txt",
                SearchOption.AllDirectories));

        try
        {
            return task1.Result;
        }
        catch (AggregateException ae)
        {
            ae.Handle(x =>
            {
                // Handle an UnauthorizedAccessException
                if (x is UnauthorizedAccessException)
                {
                    Console.WriteLine(
                        "You do not have permission to access all folders in this path.");
                    Console.WriteLine(
                        "See your network administrator or try another path.");
                }
                return x is UnauthorizedAccessException;
            });
            return Array.Empty<string>();
        }
    }
}
// The example displays the following output:
//       You do not have permission to access all folders in this path.
//       See your network administrator or try another path.
//
//       ArgumentException: The path is not of a legal form.

Obserwowanie wyjątków przy użyciu właściwości Task.Exception

Jeśli zadanie zostanie ukończone w stanie TaskStatus.Faulted, jego właściwość Exception można zbadać, aby odkryć, który konkretny wyjątek spowodował błąd. Dobrym sposobem obserwowania Exception właściwości jest użycie kontynuacji uruchamianej tylko wtedy, gdy zadanie poprzedzające zawodzi, jak pokazano w poniższym przykładzie.


public static partial class Program
{
    public static void ExceptionPropagationTwo()
    {
        _ = Task.Run(
            () => throw new CustomException("task1 faulted."))
            .ContinueWith(_ =>
            {
                if (_.Exception?.InnerException is { } inner)
                {
                    Console.WriteLine($"{inner.GetType().Name}: {inner.Message}");
                }
            }, 
            TaskContinuationOptions.OnlyOnFaulted);
        
        Thread.Sleep(500);
    }
}
// The example displays output like the following:
//        CustomException: task1 faulted.

W znaczącej aplikacji delegat kontynuacji może rejestrować szczegółowe informacje o wyjątku i ewentualnie uruchamiać nowe zadania w celu odzyskania z wyjątku. Jeśli wystąpi błąd w zadaniu, następujące wyrażenia zgłoszą wyjątek:

  • await task
  • task.Wait()
  • task.Result
  • task.GetAwaiter().GetResult()

Użyj instrukcji try-catch do obsługi i obserwacji zgłoszonych wyjątków. Alternatywnie, obserwuj wyjątek, korzystając z właściwości Task.Exception.

Ważne

Nie można jawnie przechwycić AggregateException w przypadku używania następujących wyrażeń:

  • await task
  • task.GetAwaiter().GetResult()

Zdarzenie UnobservedTaskException

W niektórych scenariuszach, takich jak w przypadku hostowania niezaufanych wtyczek, łagodne wyjątki mogą być powszechne i może być zbyt trudne ręczne obserwowanie ich wszystkich. W takich przypadkach można obsłużyć TaskScheduler.UnobservedTaskException zdarzenie. Wystąpienie System.Threading.Tasks.UnobservedTaskExceptionEventArgs przekazane do programu obsługi może służyć do zapobiegania propagacji nieobserwowanego wyjątku z powrotem do wątku przyłączania.

Zobacz też