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

작업 내에서 실행 중인 사용자 코드에 의해 throw되는 처리되지 않은 예외는 이 항목 뒷부분에 설명된 일부 경우를 제외하고는 조인하는 스레드에 다시 전파됩니다. 예외는 정적 또는 인스턴스 Task.Wait 또는 Task<TResult>.Wait 메서드 중 하나를 사용할 때 전파되며, 예외를 처리하려면 해당 호출을 try-catch 문에 넣습니다. 여러 작업에서 대기 중인 경우나 작업이 연결된 자식 작업의 부모인 경우에는 여러 개의 예외가 throw될 수 있습니다. 모든 예외를 호출하는 스레드에 다시 전파하기 위해 작업 인프라에서는 모든 예외를 AggregateException 인스턴스에 래핑합니다. AggregateException에는 InnerExceptions 속성이 있으며 이 속성을 열거하면 throw된 모든 원래 예외를 확인하고 각 예외를 개별적으로 처리하거나 처리하지 않을 수 있습니다. 예외가 하나만 throw된 경우에도 AggregateException에 예외가 래핑됩니다.

Dim task1 = Task.Factory.StartNew(Sub()
                                      Throw New MyCustomException("I'm bad, but not too bad!")
                                  End Sub)

Try
    task1.Wait()
Catch ae As AggregateException
    ' Assume we know what's going on with this particular exception.
    ' Rethrow anything else. AggregateException.Handle provides
    ' another way to express this. See later example.
    For Each ex In ae.InnerExceptions
        If TypeOf (ex) Is MyCustomException Then
            Console.WriteLine(ex.Message)
        Else
            Throw
        End If
    Next

End Try
var task1 = Task.Factory.StartNew(() =>
{
    throw new MyCustomException("I'm bad, but not too bad!");
});

try
{
    task1.Wait();
}
catch (AggregateException ae)
{
    // Assume we know what's going on with this particular exception.
    // Rethrow anything else. AggregateException.Handle provides
    // another way to express this. See later example.
    foreach (var e in ae.InnerExceptions)
    {
        if (e is MyCustomException)
        {
            Console.WriteLine(e.Message);
        }
        else
        {
            throw;
        }
    }

}

단순히 AggregateException을 catch하고 내부 예외를 관찰하지 않으면 처리되지 않은 예외를 방지할 수 있습니다. 그러나 이 방법은 비병렬 시나리오에서 기본 예외 형식을 catch하는 것과 유사하므로 이 방법은 사용하지 않는 것이 좋습니다. 예외로부터 복구하기 위한 특정 작업을 수행하지 않고 예외를 catch하면 프로그램이 불확정한 상태가 될 수 있습니다.

예외를 전파하는 작업에서 대기하거나 작업의 예외 속성에 액세스하지 않는 경우에는 작업이 가비지 수집될 때 .NET 예외 정책에 따라 예외가 에스컬레이션됩니다.

조인하는 스레드까지 버블링할 수 있도록 예외가 허용되는 경우 예외가 발생한 후에도 작업에서 일부 항목을 계속하여 처리할 수 있습니다.

참고참고

"내 코드만"을 사용하는 경우 Visual Studio에서는 예외를 throw하는 줄에서 실행을 중단하고 예외가 사용자 코드를 통해 처리되지 않았음을 알리는 오류 메시지를 표시할 수 있습니다. 이는 크게 심각한 오류는 아닙니다.F5 키를 눌러 중단된 지점부터 실행을 계속하고 아래 예제에 나와 있는 것과 같은 예외 처리 동작을 확인할 수 있습니다.맨 처음 오류 지점에서 Visual Studio가 실행을 중단하지 않도록 하려면 도구, 옵션, 디버깅, 일반을 차례로 선택하고 "내 코드만" 확인란의 선택을 취소하기만 하면 됩니다.

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

작업에 예외를 throw하는 자식 작업이 연결된 경우 이 예외는 부모 작업에 전파되기 전에 AggregateException에 래핑되며, 부모 작업은 이 예외를 다시 호출 스레드에 전파하기 전에 자체의 AggregateException에 래핑합니다. 이러한 경우 Task.Wait, Task<TResult>.Wait, WaitAny 또는 WaitAll 메서드에서 catch된 AggregateException의 AggregateException().InnerExceptions 속성에는 오류를 발생시킨 원래 예외가 아니라 하나 이상의 AggregateException 인스턴스가 포함됩니다. 중첩된 AggregateExceptions를 반복할 필요가 없도록 하려면 AggregateException() InnerExceptions 속성에 원래 예외가 포함되도록 Flatten() 메서드를 사용하여 중첩된 모든 AggregateExceptions를 제거합니다. 다음 예제에서는 중첩된 AggregateException 인스턴스가 평면화되고 한 루프에서만 처리됩니다.

' task1 will throw an AE inside an AE inside an AE
Dim task1 = Task.Factory.StartNew(Sub()
                                      Dim child1 = Task.Factory.StartNew(Sub()
                                                                             Dim child2 = Task.Factory.StartNew(Sub()
                                                                                                                    Throw New MyCustomException("Attached child2 faulted.")
                                                                                                                End Sub,
                                                                                                                TaskCreationOptions.AttachedToParent)
                                                                         End Sub,
                                                                         TaskCreationOptions.AttachedToParent)
                                      ' Uncomment this line to see the exception rethrown.
                                      ' throw new MyCustomException("Attached child1 faulted.")
                                  End Sub)
Try
    task1.Wait()
Catch ae As AggregateException
    For Each ex In ae.Flatten().InnerExceptions
        If TypeOf (ex) Is MyCustomException Then
            Console.WriteLine(ex.Message)
        Else
            Throw
        End If
    Next
    'or like this:
    '  ae.Flatten().Handle(Function(e)
    '                               Return TypeOf (e) Is MyCustomException
    '                   End Function)
End Try
// task1 will throw an AE inside an AE inside an AE
var task1 = Task.Factory.StartNew(() =>
{
    var child1 = Task.Factory.StartNew(() =>
        {
            var child2 = Task.Factory.StartNew(() =>
            {
                throw new MyCustomException("Attached child2 faulted.");
            },
            TaskCreationOptions.AttachedToParent);

            // Uncomment this line to see the exception rethrown.
            // throw new MyCustomException("Attached child1 faulted.");
        },
        TaskCreationOptions.AttachedToParent);
});

try
{
    task1.Wait();
}
catch (AggregateException ae)
{

    foreach (var e in ae.Flatten().InnerExceptions)
    {
        if (e is MyCustomException)
        {
            // Recover from the exception. Here we just
            // print the message for demonstration purposes.
            Console.WriteLine(e.Message);
        }
        else
        {
            throw;
        }
    }
    // or ...
   // ae.Flatten().Handle((ex) => ex is MyCustomException);

}

분리된 자식 작업에서의 예외

기본적으로 자식 작업은 분리된 작업으로 만들어집니다. 분리된 작업에서 throw된 예외는 바로 위 부모 작업에서 처리되거나 다시 throw되어야 하며, 연결된 자식 작업에서 예외가 다시 전파될 때와 같은 방식으로 호출 스레드에 다시 전파되지 않습니다. 최상위 부모 작업에서는 분리된 자식 작업에서 throw된 예외를 수동으로 다시 throw하여 이 예외가 AggregateException에 래핑되고 조인하는 스레드에 다시 전파되도록 할 수 있습니다.

Dim task1 = Task.Factory.StartNew(Sub()
                                      Dim nestedTask1 = Task.Factory.StartNew(Sub()
                                                                                  Throw New MyCustomException("Nested 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 MyCustomException Then
            ' Recover from the exception. Here we just
            ' print the message for demonstration purposes.
            Console.WriteLine(ex.Message)
        End If
    Next
End Try
var task1 = Task.Factory.StartNew(() =>
{

    var nested1 = Task.Factory.StartNew(() =>
    {
        throw new MyCustomException("Nested task faulted.");
    });

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

});

try
{
    task1.Wait();
}
catch (AggregateException ae)
{

    foreach (var e in ae.Flatten().InnerExceptions)
    {
        if (e is MyCustomException)
        {
            // Recover from the exception. Here we just
            // print the message for demonstration purposes.
            Console.WriteLine(e.Message);
        }
    }
}

연속 작업을 사용하여 자식 작업에서 예외를 관찰하더라도 부모 작업에서도 해당 예외를 관찰해야 합니다.

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

작업의 사용자 코드에서 취소 요청에 응답할 때의 올바른 절차는 요청이 전달된 취소 토큰을 전달하는 OperationCanceledException을 throw하는 것입니다. 예외를 전파하려고 시도하기 전에 작업 인스턴스에서는 예외의 토큰을 해당 작업이 만들어질 때 해당 작업에 전달된 토큰과 비교합니다. 두 토큰이 동일하면 작업에서는 AggregateException에 래핑된 TaskCanceledException을 전파하며, 이 예외는 내부 예외를 검사할 때 볼 수 있습니다. 하지만 조인하는 스레드가 작업에서 대기하고 있지 않은 경우에는 이 특정 예외가 전파되지 않습니다. 자세한 내용은 작업 취소를 참조하십시오.

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)
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;

var task1 = Task.Factory.StartNew(() =>
{
    CancellationToken ct = token;
    while (someCondition)
    {
        // Do some work...
        Thread.SpinWait(50000);
        ct.ThrowIfCancellationRequested();
    }
},
token);

// No waiting required.

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

Handle() 메서드를 사용하면 추가 논리를 사용하지 않고도 "처리된" 것으로 간주할 수 있는 예외를 필터링할 수 있습니다. Handle()에 제공된 사용자 대리자에서는 예외 형식, 해당 Message() 속성 또는 예외에 관한 기타 정보를 확인할 수 있으며 이를 통해 예외가 심각하지 않은 예외인지 확인할 수 있습니다. 이 대리자가 false를 반환하는 모든 예외는 Handle()이 반환된 직후 새 AggregateException 인스턴스에서 다시 throw됩니다.

다음 코드 조각에서는 내부 예외에 대한 foreach 루프를 사용합니다.

For Each ex In ae.InnerExceptions
    If TypeOf (ex) Is MyCustomException Then
        Console.WriteLine(ex.Message)
    Else
        Throw
    End If
Next
foreach (var e in ae.InnerExceptions)
{
    if (e is MyCustomException)
    {
        Console.WriteLine(e.Message);
    }
    else
    {
        throw;
    }
}

다음 코드 조각에서는 기능적으로 동일한 Handle() 메서드를 사용하는 방법을 보여 줍니다.

ae.Handle(Function(ex)
              Return TypeOf (ex) Is MyCustomException
          End Function)
ae.Handle((ex) =>
{
    return ex is MyCustomException;
});

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

작업이 Faulted 상태로 완료되는 경우 해당 Exception 속성을 확인하여 오류를 발생시킨 특정 예외를 검색할 수 있습니다. Exception 속성을 관찰하는 좋은 방법은 다음 예제에서처럼 선행 작업이 실패할 경우에만 실행되는 연속 작업을 사용하는 것입니다.

Dim task1 = Task.Factory.StartNew(Sub()
                                      Throw New MyCustomException("task1 faulted.")
                                  End Sub).ContinueWith(Sub(t)
                                                            Console.WriteLine("I have observed a {0}", _
                                                                              t.Exception.InnerException.GetType().Name)
                                                        End Sub,
                                                        TaskContinuationOptions.OnlyOnFaulted)
var task1 = Task.Factory.StartNew(() =>
{
    throw new MyCustomException("Task1 faulted.");
})
.ContinueWith((t) =>
    {
        Console.WriteLine("I have observed a {0}",
            t.Exception.InnerException.GetType().Name);
    },
    TaskContinuationOptions.OnlyOnFaulted);

실제 응용 프로그램에서 연속 작업 대리자는 예외에 대한 자세한 정보를 기록할 수 있으며 예외로부터 복구하기 위해 새 작업을 생성할 수도 있습니다.

UnobservedTaskException 이벤트

신뢰할 수 없는 플러그 인을 호스팅하는 경우와 같은 일부 시나리오에서는 심각하지 않은 예외가 자주 발생할 수 있으며 이러한 예외를 모두 수동으로 관찰하기가 매우 까다로울 수 있습니다. 이 경우 TaskScheduler.UnobservedTaskException 이벤트를 처리할 수 있습니다. 처리기에 전달된 System.Threading.Tasks.UnobservedTaskExceptionEventArgs 인스턴스를 사용하면 조인하는 스레드로 관찰되지 않은 예외가 다시 전파되는 것을 막을 수 있습니다.

참고 항목

개념

작업 병렬 라이브러리