비동기 및 병렬 작업 관리

완료됨

C# 개발자의 경우 TPL(작업 병렬 라이브러리)을 사용하면 병렬 코드를 더 쉽게 작성할 수 있습니다. 그러나 모든 코드가 병렬화에 적합한 것은 아닙니다. 예를 들어 루프가 각 반복에서 적은 양의 작업만 수행하거나 많은 반복에 대해 실행되지 않는 경우 병렬화 오버헤드로 인해 코드가 더 느리게 실행될 수 있습니다. 또한 병렬 처리는 다중 스레드 코드와 마찬가지로 프로그램 실행에 복잡성을 더합니다.

데이터 및 작업 병렬 처리의 일반적인 문제

대부분의 경우 Parallel.ForParallel.ForEach는 일반 순차적 루프에 대해 상당한 성능 향상을 제공할 수 있습니다. 그러나 루프를 병렬화하는 작업에는 순차 코드에서 일반적이지 않은 문제가 발생할 수 있는 복잡성이 발생합니다.

병렬이 항상 더 빠르다고 가정하지 마세요.

일부 경우에서 병렬 루프는 순차적 루프보다 더 느리게 실행될 수 있습니다. 기본적인 원칙은 반복 횟수가 적고 사용자 대리자가 빠른 병렬 루프는 속도가 크게 향상되지 않을 가능성이 높다는 것입니다. 그러나 많은 요인이 성능에 관련되므로 항상 실제 결과를 측정하는 것이 좋습니다.

공유 메모리 위치에 쓰기 방지

순차 코드에서는 정적 변수 또는 클래스 필드에서 읽거나 쓰는 것이 드문 일이 아닙니다. 그러나 여러 스레드가 이러한 변수에 동시에 액세스할 때마다 경합 조건에 대한 상당한 가능성이 있습니다. 잠금을 사용하여 변수에 대한 액세스를 동기화할 수 있지만 동기화의 비용으로 성능이 저하될 수 있습니다. 따라서 가능한 한 병렬 루프에서 공유된 상태에 대한 액세스를 방지하거나 최소한의 제한으로 액세스하는 것이 좋습니다. 이를 수행하는 가장 좋은 방법은 Parallel.For 변수를 사용하여 루프 실행 중 스레드 로컬 상태를 저장하는 Parallel.ForEachSystem.Threading.ThreadLocal<T>의 오버로드를 사용하는 것입니다.

과다 병렬화 방지

병렬 루프를 사용하여 소스 컬렉션을 분할하고 작업자 스레드를 동기화하는 오버헤드 비용이 발생합니다. 병렬화의 이점은 컴퓨터의 프로세서 수로 더 제한됩니다. 하나의 프로세서에서 여러 컴퓨팅 바인딩된 스레드를 실행하여 얻을 수 있는 속도는 없습니다. 따라서 루프를 과도하게 병렬화하지 않도록 주의해야 합니다.

과도한 병렬화가 발생할 수 있는 가장 일반적인 시나리오는 중첩된 루프에 있습니다. 대부분의 경우 다음 조건 중 하나 이상이 적용되지 않는 한 외부 루프만 병렬화하는 것이 가장 좋습니다.

  • 내부 루프는 긴 것으로 알려져 있습니다.
  • 각 주문에 대해 비용이 많이 드는 계산을 수행하고 있습니다.
  • 대상 시스템에는 처리를 병렬 처리하여 생성되는 스레드 수를 처리하기에 충분한 프로세서가 있는 것으로 알려져 있습니다.

모든 경우에서 최적의 쿼리 형태를 결정하는 가장 좋은 방법은 테스트하고 측정하는 것입니다.

비동기 및 병렬 작업의 예외 처리

TPL(작업 병렬 라이브러리)을 사용하여 작업을 실행하는 경우 여러 가지 방법으로 예외가 발생할 수 있습니다. 가장 일반적인 경우는 태스크에서 예외를 던지는 경우입니다. 태스크가 스레드 풀 스레드에서 실행 중이거나 주 스레드에서 실행 중일 때 예외를 throw할 수 있습니다. 두 경우 모두 예외가 호출 스레드로 다시 전파됩니다.

이 메서드를 사용하여 Task.Wait 태스크가 완료될 때까지 기다리면 태스크에서 throw된 모든 예외가 호출 스레드로 다시 전파됩니다. try/catch 블록을 사용하여 이러한 예외를 처리할 수 있습니다. 연결된 자식 작업의 부모 작업이거나 여러 작업을 기다리고 있는 경우 여러 개의 예외가 throw될 수 있습니다. 하나 이상의 예외가 발생하면, 그것들은 AggregateException 인스턴스에 래핑됩니다.

AggregateException 예외에는 InnerExceptions 속성이 있으며 이 속성을 열거하면 throw된 모든 원래 예외를 검사하고 각 예외를 개별적으로 처리하거나 처리하지 않을 수 있습니다.

다음 예제에서는 작업에서 발생된 예외를 처리하는 방법을 보여 줍니다.


public static partial class Program
{
    public static void Main()
    {
        HandleThree();
    }
    
    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;
                }
            }
        }
    }
}

// Define the CustomException class
public class CustomException : Exception
{
    public CustomException(string message) : base(message) { }
}
// The example displays the following output:
//        This exception is expected!

이 예제에서 HandleThree 메서드는 CustomException을(를) 던지는 작업을 생성합니다. try/catch 블록은 AggregateException을 catch하고 InnerExceptions 컬렉션을 반복합니다. 예외 유형 CustomException이면 콘솔에 메시지를 출력합니다. 다른 형식의 예외가 throw되면 해당 예외를 다시 throw합니다.

메서드 AggregateException.Handle를 사용하여 원래 예외를 처리할 수도 있습니다. 이 메서드는 InnerExceptions 컬렉션의 각 예외에 대해 호출되는 대리자를 받습니다. 대리자가 true를 반환하면 예외가 처리된 것으로 간주되고 컬렉션에서 제거됩니다. false를 반환하면 예외가 다시 던져집니다.

다음 예제에서는 Handle 메서드를 사용하여 태스크에서 throw된 예외를 처리하는 방법을 보여줍니다.


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

        try
        {
            task.Wait();
        }
        catch (AggregateException ae)
        {
            ae.Handle(ex =>
            {
                // Handle the custom exception.
                if (ex is CustomException)
                {
                    Console.WriteLine(ex.Message);
                    return true;
                }
                // Rethrow any other exception.
                return false;
            });
        }
    }
}

이 예제에서 HandleFour 메서드는 CustomException을(를) 던지는 작업을 생성합니다. try/catch 블록은 AggregateException를 캐치하고 Handle 메서드를 호출합니다. 대리자는 예외가 형식 CustomException인지 확인합니다. 예외가 형식 CustomException인 경우 대리자는 콘솔에 메시지를 출력하고 반환합니다 true. 응답은 true 예외가 처리되었음을 나타냅니다. 예외가 다른 형식의 예외인 경우 대리자는 false를 반환하여 예외가 다시 throw됩니다.

요약

이 단원에서는 코드가 병렬화에 적합하지 않은 상황을 설명하고 데이터 및 작업 병렬 처리의 일반적인 문제를 설명합니다. 예를 들어, 병렬 처리가 항상 더 빠르다고 가정하여 공유 메모리 위치에 데이터를 기록하고 과도한 병렬화를 수행하는 경우. 또한 이 콘텐츠는 Task.Wait 메서드와 AggregateException.Handle 메서드를 포함하여, 비동기 및 병렬 작업에서 예외를 처리하는 방법을 설명합니다.

핵심 사항

  • 모든 코드가 병렬 처리에 적합한 것은 아닙니다. 코드를 병렬화하기 전에 성능 테스트 및 측정이 필수적입니다.
  • 데이터 및 작업 병렬 처리의 일반적인 문제로는 병렬이 항상 더 빠르다 가정, 공유 메모리 위치에 쓰기 및 과다 병렬화가 포함됩니다.
  • 비동기 및 병렬 작업의 예외는 Task.Wait 메서드와 AggregateException.Handle 메서드를 사용하여 처리할 수 있습니다.
  • AggregateException 예외에는 원래 throw된 모든 예외를 검사하기 위해 열거할 수 있는 InnerExceptions 속성이 있습니다.