다음을 통해 공유


PLINQ의 잠재적인 문제

대부분의 경우 PLINQ는 순차 LINQ to Objects 쿼리에 비해 상당한 성능 향상을 제공할 수 있습니다. 그러나 쿼리 실행을 병렬화하는 작업은 순차 코드에서 일반적이지 않거나 전혀 발생하지 않는 문제를 야기할 수 있는 복잡성을 유발합니다. 이 항목에서는 PLINQ 쿼리를 작성할 때 피해야 하는 몇 가지 사례를 나열합니다.

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

병렬화로 인해 PLINQ 쿼리가 LINQ to Objects에 해당하는 쿼리보다 느리게 실행되는 경우가 있습니다. 경험적인 법칙은 소스 요소가 거의 없는 쿼리와 빠른 사용자 위임자가 있을 때 속도를 크게 높일 가능성은 낮다는 것입니다. 그러나 성능에 많은 요소가 관련되어 있으므로 PLINQ를 사용할지 여부를 결정하기 전에 실제 결과를 측정하는 것이 좋습니다. 자세한 내용은 PLINQ의 속도 향상 이해를 참조하세요.

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

순차적 코드에서는 정적 변수 또는 클래스 필드에서 읽거나 쓰는 것은 일반적입니다. 그러나 여러 스레드가 해당 변수에 동시에 액세스할 때마다 경합 상태가 발생할 가능성이 큽니다. 잠금을 사용하여 변수에 대한 액세스를 동기화할 수 있지만 동기화의 비용으로 성능이 저하될 수 있습니다. 따라서 PLINQ 쿼리에서 공유 상태에 대한 액세스를 최대한 피하거나 최소한 제한하는 것이 좋습니다.

과다 병렬화 방지

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

오버 병렬화가 발생할 수 있는 가장 일반적인 시나리오는 다음 코드 조각과 같이 중첩된 쿼리에 있습니다.

var q = from cust in customers.AsParallel()
        from order in cust.Orders.AsParallel()
        where order.OrderDate > date
        select new { cust, order };
Dim q = From cust In customers.AsParallel()
        From order In cust.Orders.AsParallel()
        Where order.OrderDate > aDate
        Select New With {cust, order}

이 경우 다음 조건 중 하나 이상이 적용되지 않는 한 외부 데이터 원본(고객)만 병렬화하는 것이 가장 좋습니다.

  • 내부 데이터 소스인 cust.Orders가 매우 긴 것으로 알려져 있습니다.

  • 각 주문에서 비용이 많이 드는 계산을 수행하고 있습니다. (이 예제에 나와 있는 작업은 비용이 많이 들지 않습니다.)

  • 대상 시스템에는 쿼리를 병렬화하여 생성되는 스레드 수를 처리할 수 있는 충분한 프로세서가 있는 것으로 알려져 있습니다 cust.Orders.

모든 경우에서 최적의 쿼리 형태를 결정하는 가장 좋은 방법은 테스트하고 측정하는 것입니다. 자세한 내용은 방법: PLINQ 쿼리 성능 측정을 참조하세요.

스레드로부터 안전하지 않은 메서드 호출 방지

PLINQ 쿼리에서 스레드로부터 안전하지 않은 인스턴스 메서드에 쓰면 데이터 손상이 발생할 수 있으며, 이로 인해 프로그램에서 검색되지 않을 수도 있고 감지되지 않을 수도 있습니다. 예외가 발생할 수도 있습니다. 다음 예제에서는 여러 스레드가 FileStream.Write 메서드를 동시에 호출하려고 합니다. 이는 클래스에서 지원되지 않습니다.

Dim fs As FileStream = File.OpenWrite(…)
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(Sub(x) fs.Write(x))
FileStream fs = File.OpenWrite(...);
a.AsParallel().Where(...).OrderBy(...).Select(...).ForAll(x => fs.Write(x));

스레드로부터 안전한 메서드에 대한 호출 제한

.NET에서 대부분의 정적 메서드는 스레드로부터 안전하고 여러 스레드에서 동시에 호출될 수 있습니다. 그러나 이러한 경우에도 관련된 동기화로 인해 쿼리 속도가 상당히 느려질 수 있습니다.

비고

쿼리에서 WriteLine에 일부 호출을 삽입하여 이를 직접 테스트할 수 있습니다. 이 메서드는 설명서 예제에서 데모용으로 사용되지만 PLINQ 쿼리에서는 사용하지 마세요.

불필요한 순서 지정 작업 방지

PLINQ는 쿼리를 병렬로 실행하면 소스 시퀀스를 여러 스레드에서 동시에 작동할 수 있는 파티션으로 나눕니다. 기본적으로 파티션이 처리되고 결과가 전달되는 순서는 예측할 수 없습니다(예: OrderBy연산자 제외). 원본 시퀀스의 순서를 유지하도록 PLINQ에 지시할 수 있지만 성능에 부정적인 영향을 줍니다. 가능한 경우 순서 유지에 의존하지 않도록 쿼리를 구성하는 것이 가장 좋습니다. 자세한 내용은 PLINQ의 Order 유지를 참조하세요.

가능한 경우 ForEach에 ForAll을 선호합니다.

PLINQ가 여러 스레드에서 쿼리를 실행하더라도, 결과를 foreach 루프(Visual Basic의 For Each)에서 사용하는 경우 쿼리 결과를 다시 하나의 스레드로 병합하고 열거자를 통해 직렬로 액세스해야 합니다. 경우에 따라 이 작업을 피할 수 없습니다. 그러나 가능하면 메서드를 ForAll 사용하여 각 스레드가 스레드로부터 안전한 컬렉션(예: System.Collections.Concurrent.ConcurrentBag<T>스레드로부터 안전한 컬렉션)에 기록하여 자체 결과를 출력할 수 있도록 합니다.

동일한 문제가 적용됩니다.Parallel.ForEach 즉, source.AsParallel().Where().ForAll(...)Parallel.ForEach(source.AsParallel().Where(), ...)보다 강력하게 선호해야 합니다.

스레드 결합 문제에 주의하세요

STA(단일 스레드 아파트) 구성 요소에 대한 COM 상호 운용성, Windows Forms 및 WPF(Windows Presentation Foundation)와 같은 일부 기술은 특정 스레드에서 실행하는 코드를 필요로 하는 스레드 선호도 제한 사항이 적용됩니다. 예를 들어 Windows Forms 및 WPF에서 컨트롤은 작성된 스레드에서만 액세스될 수 있습니다. PLINQ 쿼리에서 Windows Forms 컨트롤의 공유 상태에 액세스하려고 하면 디버거에서 실행하는 경우 예외가 발생합니다. (이 설정은 해제할 수 있습니다.) 그러나 쿼리가 UI 스레드에서 사용되는 경우 해당 코드가 하나의 스레드에서만 실행되기 때문에 쿼리 결과를 열거하는 루프에서 foreach 컨트롤에 액세스할 수 있습니다.

ForEach, For 및 ForAll의 반복이 항상 병렬로 실행된다고 가정하지 마세요.

Parallel.For, Parallel.ForEach, 또는 ForAll 루프의 개별 반복이 병렬로 실행될 수도 있지만 반드시 병렬로 실행될 필요는 없다는 점을 명심해야 합니다. 따라서 반복의 병렬 실행 또는 특정 순서로 반복 실행의 정확성에 의존하는 코드를 작성하지 마세요.

예를 들어 다음 코드는 교착 상태의 가능성이 있습니다.

Dim mre = New ManualResetEventSlim()
Enumerable.Range(0, Environment.ProcessorCount * 100).AsParallel().ForAll(Sub(j)
   If j = Environment.ProcessorCount Then
       Console.WriteLine("Set on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j)
       mre.Set()
   Else
       Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j)
       mre.Wait()
   End If
End Sub) ' deadlocks
ManualResetEventSlim mre = new ManualResetEventSlim();
Enumerable.Range(0, Environment.ProcessorCount * 100).AsParallel().ForAll((j) =>
{
    if (j == Environment.ProcessorCount)
    {
        Console.WriteLine("Set on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j);
        mre.Set();
    }
    else
    {
        Console.WriteLine("Waiting on {0} with value of {1}", Thread.CurrentThread.ManagedThreadId, j);
        mre.Wait();
    }
}); //deadlocks

이 예제에서는 하나의 반복이 이벤트를 설정하고 다른 모든 반복은 이벤트를 기다립니다. 대기 중인 반복은 이벤트 설정 반복이 완료될 때까지 완료할 수 없습니다. 그러나 대기 중인 반복은 이벤트 설정 반복이 실행될 기회를 갖기 전에 병렬 루프를 실행하는 데 사용되는 모든 스레드를 차단할 수 있습니다. 이로 인해 교착 상태가 발생하고 이벤트 설정 반복은 실행되지 않으며 대기 중인 반복은 시작되지 않습니다.

특히 병렬 루프의 하나의 반복은 다른 루프의 반복이 진행되기를 기다리면 안 됩니다. 병렬 루프가 반대 순서로 순차적으로 반복되도록 결정하는 경우 교착 상태가 발생합니다.

참고하십시오