다음을 통해 공유


예외 처리(작업 병렬 라이브러리)

작업 내에서 실행 중인 사용자 코드에서 throw되는 처리되지 않은 예외는 이 항목의 뒷부분에 설명된 특정 시나리오를 제외하고 호출 스레드로 다시 전파됩니다. 예외는 정적 또는 인스턴스 Task.Wait 메서드 중 하나를 사용하고 문에 try/catch 호출을 묶어 처리할 때 전파됩니다. 작업이 연결된 자식 작업의 부모이거나 여러 작업을 기다리는 경우 여러 예외가 발생할 수 있습니다.

모든 예외를 호출 스레드로 다시 전파하기 위해, 작업 인프라는 AggregateException 인스턴스 안에 이를 래핑합니다. 예외에는 throw된 모든 원래 예외를 검사하고 각각을 개별적으로 처리(또는 처리하지 않음)하기 위해 열거할 수 있는 AggregateException 속성이 InnerExceptions 있습니다. 메서드 AggregateException.Handle를 사용하여 원래 예외를 처리할 수도 있습니다.

예외가 하나만 발생하더라도, 다음 예에서 볼 수 있듯이, 예외로 AggregateException에 래핑됩니다.


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!
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Run(Sub() Throw New CustomException("This exception is expected!"))

        Try
            task1.Wait()
        Catch ae As AggregateException
            For Each ex In ae.InnerExceptions
                ' Handle the custom exception.
                If TypeOf ex Is CustomException Then
                    Console.WriteLine(ex.Message)
                    ' Rethrow any other exception.
                Else
                    Throw ex
                End If
            Next
        End Try
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays the following output:
'       This exception is expected!

내부 예외들을 관찰하지 않고 AggregateException를 캐치하기만 함으로써 처리되지 않은 예외를 피할 수 있습니다. 그러나 병렬이 아닌 시나리오에서 기본 Exception 형식을 catch하는 것과 유사하므로 이 작업을 수행하지 않는 것이 좋습니다. 특정 작업을 수행하지 않고 예외를 catch하여 복구하려면 프로그램을 확정되지 않은 상태로 둘 수 있습니다.

메서드를 호출 Task.Wait 하여 태스크가 완료될 때까지 기다리지 않으려면 다음 예제와 같이 태스크 AggregateException 의 속성에서 예외를 검색 Exception 할 수도 있습니다. 자세한 내용은 이 항목의 Task.Exception 속성 섹션을 사용하여 예외 관찰 을 참조하세요.


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!
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Run(Sub() Throw New CustomException("This exception is expected!"))

        While Not task1.IsCompleted
        End While

        If task1.Status = TaskStatus.Faulted Then
            For Each ex In task1.Exception.InnerExceptions
                ' Handle the custom exception.
                If TypeOf ex Is CustomException Then
                    Console.WriteLine(ex.Message)
                    ' Rethrow any other exception.
                Else
                    Throw ex
                End If
            Next
        End If
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays the following output:
'       This exception is expected!

주의

앞의 예제 코드에는 작업이 완료되었는지를 확인하기 위해 태스크의 while 속성을 폴링하는 Task.IsCompleted 루프가 포함되어 있습니다. 이 작업은 매우 비효율적이므로 프로덕션 코드에서 수행해서는 안 됩니다.

예외를 전파하는 작업을 기다리거나 해당 Exception 속성에 액세스하지 않으면 작업이 가비지 수집될 때 .NET 예외 정책에 따라 예외가 에스컬레이션됩니다.

예외를 조인 스레드로 다시 버블업할 수 있는 경우 예외가 발생한 후에도 태스크가 일부 항목을 계속 처리할 수 있습니다.

비고

"내 코드만"을 사용하도록 설정하면 경우에 따라 Visual Studio가 예외를 throw하는 줄에서 중단되고 "사용자 코드에서 처리되지 않는 예외"라는 오류 메시지가 표시됩니다. 이 오류는 무해합니다. F5 키를 눌러 계속하면 이러한 예제에 설명된 예외 처리 동작을 볼 수 있습니다. Visual Studio가 첫 번째 오류 시 중단하지 않도록 하려면, 도구, 옵션, 디버깅, 일반에서 내 코드만 사용 확인란의 선택을 해제하십시오.

연결된 자식 작업 및 중첩된 AggregateExceptions

태스크에 예외를 throw하는 연결된 자식 태스크가 있는 경우, 그 예외는 부모 태스크로 전파되기 전에 AggregateException 안에 래핑됩니다. 그런 다음, 부모 태스크는 해당 예외를 호출 스레드로 다시 전파하기 전에 자체 AggregateException 안에 다시 래핑합니다. 이러한 경우 InnerExceptions, AggregateException 또는 Task.Wait 메서드에서 catch된 예외의 WaitAny 속성에는 오류를 발생시킨 원래 예외가 아닌 하나 이상의 WaitAll 인스턴스가 포함됩니다. 중첩된 AggregateException 예외를 반복하지 않도록 하려면 이 메서드를 사용하여 Flatten 속성에 원래 예외가 AggregateException 포함되도록 중첩된 AggregateException.InnerExceptions 모든 예외를 제거할 수 있습니다. 다음 예제에서는 중첩된 AggregateException 인스턴스가 하나의 루프에서만 평면화되고 처리됩니다.


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.
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Factory.StartNew(Sub()
                                              Dim child1 = Task.Factory.StartNew(Sub()
                                                                                     Dim child2 = Task.Factory.StartNew(Sub()
                                                                                                                            Throw New CustomException("Attached child2 faulted.")
                                                                                                                        End Sub,
                                                                                                                        TaskCreationOptions.AttachedToParent)
                                                                                     Throw New CustomException("Attached child1 faulted.")
                                                                                 End Sub,
                                                                                 TaskCreationOptions.AttachedToParent)
                                          End Sub)

        Try
            task1.Wait()
        Catch ae As AggregateException
            For Each ex In ae.Flatten().InnerExceptions
                If TypeOf ex Is CustomException Then
                    Console.WriteLine(ex.Message)
                Else
                    Throw
                End If
            Next
        End Try
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays the following output:
'       Attached child1 faulted.
'       Attached child2 faulted.

AggregateException.Flatten 메서드를 사용하여, 다음 예제와 같이 하나의 AggregateException 인스턴스에서 여러 태스크에 의해 throw된 여러 개의 AggregateException 인스턴스에서 발생한 내부 예외를 다시 throw할 수도 있습니다.

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.
Imports System.Collections.Generic
Imports System.IO
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Try
            ExecuteTasks()
        Catch ae As AggregateException
            For Each e In ae.InnerExceptions
                Console.WriteLine("{0}:{2}   {1}", e.GetType().Name, e.Message,
                                  vbCrLf)
            Next
        End Try
    End Sub

    Sub ExecuteTasks()
        ' Assume this is a user-entered String.
        Dim path = "C:\"
        Dim tasks As New List(Of Task)

        tasks.Add(Task.Run(Function()
                               ' This should throw an UnauthorizedAccessException.
                               Return Directory.GetFiles(path, "*.txt",
                                                         SearchOption.AllDirectories)
                           End Function))

        tasks.Add(Task.Run(Function()
                               If path = "C:\" Then
                                   Throw New ArgumentException("The system root is not a valid path.")
                               End If
                               Return {".txt", ".dll", ".exe", ".bin", ".dat"}
                           End Function))

        tasks.Add(Task.Run(Sub()
                               Throw New NotImplementedException("This operation has not been implemented.")
                           End Sub))

        Try
            Task.WaitAll(tasks.ToArray)
        Catch ae As AggregateException
            Throw ae.Flatten()
        End Try
    End Sub
End Module
' 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.

분리된 자식 작업에서 발생하는 예외

기본적으로 자식 작업은 분리된 것으로 만들어집니다. 분리된 작업에서 throw된 예외는 즉시 부모 작업에서 처리하거나 다시 throw해야 합니다. 연결된 자식 작업이 다시 전파되는 것과 같은 방식으로 호출 스레드로 다시 전파되지 않습니다. 최상위 부모는 분리된 자식의 예외를 수동으로 재던져서 그 예외가 AggregateException에 래핑되어 호출 스레드로 다시 전파되도록 할 수 있습니다.


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.
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Run(Sub()
                                 Dim nestedTask1 = Task.Run(Sub()
                                                                Throw New CustomException("Detached child task faulted.")
                                                            End Sub)
                                 ' Here the exception will be escalated back to joining thread.
                                 ' We could use try/catch here to prevent that.
                                 nestedTask1.Wait()
                             End Sub)

        Try
            task1.Wait()
        Catch ae As AggregateException
            For Each ex In ae.Flatten().InnerExceptions
                If TypeOf ex Is CustomException Then
                    ' Recover from the exception. Here we just
                    ' print the message for demonstration purposes.
                    Console.WriteLine(ex.Message)
                End If
            Next
        End Try
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays the following output:
'       Detached child task faulted.

컨티뉴에이션을 사용하여 자식 작업에서 예외를 관찰하는 경우에도 부모 작업에서 예외를 관찰해야 합니다.

협조적 취소를 나타내는 예외

태스크의 사용자 코드가 취소 요청에 응답하는 경우 올바른 절차는 OperationCanceledException 요청이 전달된 취소 토큰을 전달하는 것입니다. 예외를 전파하기 전에 작업 인스턴스는 예외의 토큰을 만들 때 전달된 토큰과 비교합니다. 동일한 경우 태스크는 TaskCanceledException 내에 래핑된 AggregateException을(를) 전파하며, 내부 예외를 검사할 때 볼 수 있습니다. 그러나 호출 스레드가 태스크에서 대기하지 않는 경우 이 특정 예외는 전파되지 않습니다. 자세한 내용은 작업 취소참조하세요.

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();
Dim someCondition As Boolean = True
Dim tokenSource = New CancellationTokenSource()
Dim token = tokenSource.Token

Dim task1 = Task.Factory.StartNew(Sub()
                                      Dim ct As CancellationToken = token
                                      While someCondition = True
                                          ' Do some work...
                                          Thread.SpinWait(500000)
                                          ct.ThrowIfCancellationRequested()
                                      End While
                                  End Sub,
                                  token)

핸들 메서드를 사용하여 내부 예외 필터링

이 메서드를 AggregateException.Handle 사용하여 추가 논리를 사용하지 않고 "처리됨"으로 처리할 수 있는 예외를 필터링할 수 있습니다. AggregateException.Handle(Func<Exception,Boolean>) 메서드에 제공된 사용자 대리자에서 예외의 유형, 해당 Message 속성 또는 이는 무해한지 결정할 수 있는 기타 정보가 있는지 검사할 수 있습니다. 대리자가 false을(를) 반환하는 예외는 AggregateException 메서드가 반환된 직후 새 AggregateException.Handle 인스턴스에서 재throw됩니다.

다음 예제는 컬렉션의 각 예외를 검사하는 이 항목의 첫 번째 예제와 AggregateException.InnerExceptions 기능적으로 동일합니다. 대신, 이 예외 처리기는 각 예외에 대해 AggregateException.Handle 메서드 객체를 호출하여 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!
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Run(Sub() Throw New CustomException("This exception is expected!"))

        Try
            task1.Wait()
        Catch ae As AggregateException
            ' Call the Handle method to handle the custom exception,
            ' otherwise rethrow the exception.
            ae.Handle(Function(e)
                          If TypeOf e Is CustomException Then
                              Console.WriteLine(e.Message)
                          End If
                          Return TypeOf e Is CustomException
                      End Function)
        End Try
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays the following output:
'       This exception is expected!

다음은 파일을 열거할 때 예외에 대한 특수 처리를 제공하기 위해 AggregateException.Handle 메서드를 사용하는 좀 더 완전한 예제입니다.

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.
Imports System.IO
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        ' This should throw an UnauthorizedAccessException.
        Try
            Dim files = GetAllFiles("C:\")
            If files IsNot Nothing Then
                For Each file In files
                    Console.WriteLine(file)
                Next
            End If
        Catch ae As AggregateException
            For Each ex In ae.InnerExceptions
                Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message)
            Next
        End Try
        Console.WriteLine()

        ' This should throw an ArgumentException.
        Try
            For Each s In GetAllFiles("")
                Console.WriteLine(s)
            Next
        Catch ae As AggregateException
            For Each ex In ae.InnerExceptions
                Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message)
            Next
        End Try
        Console.WriteLine()
    End Sub

    Function GetAllFiles(ByVal path As String) As String()
        Dim task1 = Task.Run(Function()
                                 Return Directory.GetFiles(path, "*.txt",
                                                           SearchOption.AllDirectories)
                             End Function)
        Try
            Return task1.Result
        Catch ae As AggregateException
            ae.Handle(Function(x)
                          ' Handle an UnauthorizedAccessException
                          If TypeOf x Is UnauthorizedAccessException Then
                              Console.WriteLine("You do not have permission to access all folders in this path.")
                              Console.WriteLine("See your network administrator or try another path.")
                          End If
                          Return TypeOf x Is UnauthorizedAccessException
                      End Function)
        End Try
        Return Array.Empty(Of String)()
    End Function
End Module
' 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.

Task.Exception 속성을 사용하여 예외 관찰

태스크가 TaskStatus.Faulted 상태에서 완료되면, Exception 속성을 검사하여 어떤 특정 예외가 오류를 발생시켰는지 알아낼 수 있습니다. 속성을 관찰 Exception 하는 좋은 방법은 다음 예제와 같이 선행 작업이 오류가 발생한 경우에만 실행되는 연속 작업을 사용하는 것입니다.


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.
Imports System.Threading
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task.Factory.StartNew(Sub()
                                              Throw New CustomException("task1 faulted.")
                                          End Sub).
                    ContinueWith(Sub(t)
                                     Console.WriteLine("{0}: {1}",
                                                     t.Exception.InnerException.GetType().Name,
                                                     t.Exception.InnerException.Message)
                                 End Sub, TaskContinuationOptions.OnlyOnFaulted)

        Thread.Sleep(500)
    End Sub
End Module

Class CustomException : Inherits Exception
    Public Sub New(s As String)
        MyBase.New(s)
    End Sub
End Class
' The example displays output like the following:
'       CustomException: task1 faulted.

의미 있는 애플리케이션에서 연속 대리자는 예외에 대한 자세한 정보를 기록하고 예외에서 복구할 새 작업을 생성할 수 있습니다. 태스크에 오류가 발생하면 다음 식은 예외를 발생시킵니다.

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

try-catch 문장을 사용하여 throw된 예외를 처리하고 관찰하십시오. 또는 Task.Exception 속성에 액세스하여 예외를 관찰합니다.

중요합니다

다음 표현을 사용할 때는 AggregateException를 명시적으로 catch할 수 없습니다.

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

UnobservedTaskException 이벤트

신뢰할 수 없는 플러그 인을 호스팅하는 경우와 같은 일부 시나리오에서는 무해한 예외가 일반적일 수 있으며 모든 것을 수동으로 관찰하기가 너무 어려울 수 있습니다. 이러한 경우 TaskScheduler.UnobservedTaskException 이벤트를 처리할 수 있습니다. System.Threading.Tasks.UnobservedTaskExceptionEventArgs 처리기에 전달되는 인스턴스를 사용하여 관찰되지 않은 예외가 조인 스레드로 다시 전파되는 것을 방지할 수 있습니다.

참고하십시오