다음을 통해 공유


PLINQ 소개

병렬 쿼리 정의

.NET Framework 버전 3.0에서 도입된 LINQ(통합 언어 쿼리)에는 형식 안정적인 방식으로 System.Collections.IEnumerable 또는 System.Collections.Generic.IEnumerable<T> 데이터 소스를 쿼리하기 위한 통합 모델이 사용됩니다. LINQ to Objects는 List<T> 및 배열과 같은 메모리 내 컬렉션에 대해 실행되는 LINQ 쿼리의 이름입니다. 이 문서에서는 사용자가 LINQ의 기본적인 개념을 이해하고 있다고 가정합니다. 자세한 내용은 LINQ(통합 언어 쿼리)를 참조하십시오.

PLINQ(병렬 LINQ)는 LINQ 패턴의 병렬 구현입니다. PLINQ 쿼리는 여러 가지 면에서 비병렬 LINQ to Objects 쿼리와 유사합니다. PLINQ 쿼리는 순차적 LINQ 쿼리와 마찬가지로 메모리 내 IEnumerable 또는 IEnumerable<T> 데이터 소스에 대해 작동하며 지연된 실행을 사용합니다. 즉, 쿼리가 열거될 때까지 실행이 시작되지 않습니다. 주요 차이점은 PLINQ에서는 시스템의 모든 프로세서를 최대한으로 활용하려고 한다는 점입니다. 이를 위해 PLINQ에서는 데이터 소스를 여러 세그먼트로 분할한 다음 개별 작업자 스레드에 있는 각 세그먼트에 대한 쿼리를 여러 프로세서에서 병렬로 실행합니다. 대부분의 경우 쿼리를 병렬로 실행하면 실행 속도가 상당히 빨라집니다.

일부 쿼리 유형의 경우 PLINQ를 사용하면 병렬 실행을 통해 레거시 코드를 사용할 때보다 성능을 크게 향상시킬 수 있습니다. 대개 데이터 소스에 AsParallel 쿼리 작업을 추가하기만 하면 됩니다. 그러나 병렬 처리에는 복잡한 과정이 필요하므로 일부 쿼리 작업은 PLINQ를 사용할 경우 더 느리게 실행될 수도 있습니다. 일부 쿼리는 실제로 병렬화함으로써 속도가 더 느려집니다. 따라서 순서 지정 등의 문제가 병렬 쿼리에 주는 영향을 이해하고 있어야 합니다. 자세한 내용은 PLINQ의 속도 향상 이해를 참조하십시오.

참고참고

이 문서에서는 람다 식을 사용하여 PLINQ에 대리자를 정의합니다.C# 또는 Visual Basic의 람다 식에 익숙하지 않으면 PLINQ 및 TPL의 람다 식을 참조하십시오.

이 문서에서는 주요 PLINQ 클래스에 대해 간략히 설명하고 PLINQ 쿼리를 만드는 방법에 대해 설명합니다. 각 단원에는 보다 자세한 정보와 코드 예제를 볼 수 있는 링크가 포함되어 있습니다.

ParallelEnumerable 클래스

System.Linq.ParallelEnumerable 클래스는 PLINQ의 거의 모든 기능을 노출합니다. 이 클래스와 System.Linq 네임스페이스의 나머지 형식은 System.Core.dll 어셈블리로 컴파일됩니다. Visual Studio의 기본 C# 및 Visual Basic 프로젝트는 모두 이 어셈블리를 참조하고 이 네임스페이스를 가져옵니다.

ParallelEnumerable은 LINQ to Objects에서 지원하는 표준 쿼리 연산자를 각각 병렬화하려고 하지는 않지만 이러한 연산자의 구현을 포함합니다. LINQ에 익숙하지 않을 경우에는 LINQ 소개를 참조하십시오.

ParallelEnumerable 클래스에는 표준 쿼리 연산자 외에도 병렬 실행과 관련된 동작을 사용할 수 있게 해 주는 일련의 메서드가 포함되어 있습니다. 다음 표에서는 이러한 PLINQ 관련 메서드를 보여 줍니다.

ParallelEnumerable 연산자

설명

AsParallel

PLINQ의 진입점입니다. 가능한 경우 쿼리의 나머지 부분이 병렬화되도록 지정합니다.

AsSequential<TSource>

쿼리의 나머지 부분이 비병렬 LINQ 쿼리처럼 순차적으로 실행되도록 지정합니다.

AsOrdered

orderby(Vlsual Basic의 경우 Order By) 절을 사용하는 등의 방법으로 PLINQ에서 쿼리의 나머지 부분에 대해 또는 순서가 변경될 때까지 소스 시퀀스의 순서를 유지하도록 지정합니다.

AsUnordered<TSource>

PLINQ에서 쿼리의 나머지 부분에 대해 소스 시퀀스의 순서를 유지할 필요가 없도록 지정합니다.

WithCancellation<TSource>

PLINQ에서 제공된 취소 토큰을 정기적으로 모니터링하고 취소가 요청된 경우 실행을 취소하도록 지정합니다.

WithDegreeOfParallelism<TSource>

PLINQ에서 쿼리를 병렬화하는 데 사용할 최대 프로세서 수를 지정합니다.

WithMergeOptions<TSource>

가능한 경우 PLINQ에서 병렬 결과를 다시 소비 스레드의 한 시퀀스에만 병합하는 방법에 대한 힌트를 제공합니다.

WithExecutionMode<TSource>

기본 동작이 쿼리를 순차적으로 실행하는 것인 경우에도 PLINQ에서 쿼리를 병렬화할지 여부를 지정합니다.

ForAll<TSource>

쿼리 결과를 반복할 때와는 달리 결과를 먼저 소비 스레드에 다시 병합하지 않고 병렬로 처리할 수 있도록 하는 다중 스레드 열거형 메서드입니다.

Aggregate 오버로드

PLINQ에 고유한 오버로드로서, 스레드 로컬 파티션에 대한 중간 집계와 모든 파티션의 결과를 결합하는 최종 집계 함수를 사용할 수 있게 해 줍니다.

옵트인(Opt In) 모델

쿼리를 작성하는 경우 다음 예제와 같이 데이터 소스의 ParallelEnumerable.AsParallel 확장 메서드를 호출하여 PLINQ에 옵트인(opt in)합니다.

Dim source = Enumerable.Range(1, 10000)

' Opt in to PLINQ with AsParallel
Dim evenNums = From num In source.AsParallel()
               Where Compute(num) > 0
               Select num
var source = Enumerable.Range(1, 10000);


// Opt-in to PLINQ with AsParallel
var evenNums = from num in source.AsParallel()
               where Compute(num) > 0
               select num;

AsParallel 확장 메서드는 후속 쿼리 연산자(이 경우 where 및 select)를 System.Linq.ParallelEnumerable 구현에 바인딩합니다.

실행 모드

기본적으로 PLINQ는 보수적입니다. 런타임에 PLINQ 인프라에서는 쿼리의 전반적인 구조를 분석합니다. 병렬화를 통해 쿼리 속도가 향상될 수 있으면 PLINQ에서는 소스 시퀀스를 동시에 실행할 수 있는 여러 작업으로 분할합니다. 쿼리를 병렬화하는 것이 안전하지 않은 경우 PLINQ에서는 쿼리를 순차적으로만 실행합니다. 부담이 클 수 있는 병렬 알고리즘이나 부담이 적은 순차적 알고리즘 중에서 선택할 수 있는 경우 PLINQ에서는 기본적으로 순차적 알고리즘을 선택합니다. WithExecutionMode<TSource> 메서드와 System.Linq.ParallelExecutionMode 열거형을 사용하면 PLINQ에서 병렬 알고리즘을 선택하도록 지정할 수 있습니다. 이 방법은 테스트와 측정을 통해 특정 쿼리가 병렬 모드에서 더 빠르게 실행되는 것으로 확인된 경우에 유용합니다. 자세한 내용은 방법: PLINQ에 실행 모드 지정을 참조하십시오.

병렬 처리 수준

기본적으로 PLINQ에서는 호스트 컴퓨터의 모든 프로세서(최대 64개)를 사용합니다. WithDegreeOfParallelism<TSource> 메서드를 사용하면 PLINQ에서 프로세서를 지정된 수 이하로만 사용하도록 할 수 있습니다. 이 방법은 컴퓨터에서 실행 중인 다른 프로세스가 일정량의 CPU 시간을 받도록 하려는 경우에 유용합니다. 다음 코드 조각에서는 최대 2개의 프로세서를 사용하도록 쿼리를 제한합니다.

Dim query = From item In source.AsParallel().WithDegreeOfParallelism(2)
            Where Compute(item) > 42
            Select item
var query = from item in source.AsParallel().WithDegreeOfParallelism(2)
            where Compute(item) > 42
            select item;

쿼리에서 파일 I/O와 같이 CPU 바인딩되지 않은 작업을 상당히 많이 수행할 경우에는 병렬 처리 수준을 컴퓨터의 코어 수보다 높게 지정하는 것이 좋을 수 있습니다.

순서가 지정된 병렬 쿼리와 순서가 지정되지 않은 병렬 쿼리 비교

일부 쿼리의 경우 쿼리 연산자가 생성하는 결과에서 소스 시퀀스의 순서를 유지해야 합니다. PLINQ에서는 이를 위해 AsOrdered 연산자를 제공합니다. AsOrderedAsSequential<TSource>과는 다릅니다. AsOrdered 시퀀스는 여전히 병렬로 처리되지만 해당 결과가 버퍼링되고 정렬됩니다. 순서 유지를 위해서는 일반적으로 추가 작업이 필요하므로 AsOrdered 시퀀스는 기본 AsUnordered<TSource> 시퀀스보다 느리게 처리될 수 있습니다. 특정 작업을 순서가 지정된 병렬 처리로 실행할 경우 순차적으로 실행할 때보다 속도가 빨라질지 여부는 여러 요인에 따라 결정됩니다.

다음 코드 예제에서는 순서 유지 기능을 사용하는 방법을 보여 줍니다.

        Dim evenNums = From num In numbers.AsParallel().AsOrdered()
                      Where num Mod 2 = 0
                      Select num



            evenNums = from num in numbers.AsParallel().AsOrdered()
                       where num % 2 == 0
                       select num;


자세한 내용은 PLINQ에서 순서 유지를 참조하십시오.

병렬 쿼리와순차적 쿼리 비교

일부 작업에서는 소스 데이터가 순차적으로 전달되어야 합니다. ParallelEnumerable 쿼리 연산자는 필요할 경우 자동으로 순차 모드로 되돌립니다. 순차적으로 실행해야 하는 사용자 정의 쿼리 연산자 및 사용자 대리자를 위해 PLINQ에서는 AsSequential<TSource> 메서드를 제공합니다. AsSequential<TSource>을 사용할 경우 쿼리의 모든 후속 연산자는 AsParallel이 다시 호출될 때까지 순차적으로 실행됩니다. 자세한 내용은 방법: 병렬 및 순차적 LINQ 쿼리 결합을 참조하십시오.

쿼리 결과 병합 옵션

PLINQ 쿼리가 병렬로 실행될 경우 각 작업자 스레드의 결과는 foreach 루프(Visual Basic의 경우 For Each)에서 사용하거나 목록 또는 배열에 삽입할 수 있도록 주 스레드에 다시 병합되어야 합니다. 일부 경우에는 결과 생성을 보다 빨리 시작하는 옵션과 같이 특정 종류의 병합 옵션을 지정하는 것이 좋을 수 있습니다. 이를 위해 PLINQ에서는 WithMergeOptions<TSource> 메서드와 ParallelMergeOptions 열거형을 지원합니다. 자세한 내용은 PLINQ의 병합 옵션을 참조하십시오.

ForAll 연산자

순차적 LINQ 쿼리에서는 쿼리가 foreach(Visual Basic의 경우 For Each) 루프에서 열거되거나 ToList<TSource>, ToTSource> 또는 ToDictionary 등의 메서드를 호출하여 열거될 때까지 실행이 지연됩니다. PLINQ에서는 foreach를 사용하여 쿼리를 실행하고 결과를 반복할 수도 있습니다. 그러나 foreach 자체는 병렬로 실행되지 않으므로 모든 병렬 작업의 출력을 해당 루프가 실행되는 스레드에 다시 병합해야 합니다. PLINQ에서는 쿼리 결과의 최종 순서를 유지해야 할 때와, 각 요소에 대해 Console.WriteLine을 호출하는 경우와 같이 순차적 방식으로 결과를 처리해야 할 때마다 foreach를 사용할 수 있습니다. 순서를 유지할 필요가 없고 결과 처리 자체를 병렬화할 수 있는 경우 쿼리를 보다 빠르게 실행하려면 ForAll<TSource> 메서드를 사용하여 PLINQ 쿼리를 실행합니다. ForAll<TSource>에서는 이 최종 병합 단계를 수행하지 않습니다. 다음 코드 예제에서는 ForAll<TSource> 메서드를 사용하는 방법을 보여 줍니다. 여기서는 항목을 제거하려 시도하지 않고 여러 스레드를 동시에 추가할 수 있도록 최적화된 System.Collections.Concurrent.ConcurrentBag<T>을 사용합니다.

Dim nums = Enumerable.Range(10, 10000)
Dim query = From num In nums.AsParallel()
            Where num Mod 10 = 0
            Select num

' Process the results as each thread completes
' and add them to a System.Collections.Concurrent.ConcurrentBag(Of Int)
' which can safely accept concurrent add operations
query.ForAll(Sub(e) concurrentBag.Add(Compute(e)))

var nums = Enumerable.Range(10, 10000);


var query = from num in nums.AsParallel()
            where num % 10 == 0
            select num;

// Process the results as each thread completes
// and add them to a System.Collections.Concurrent.ConcurrentBag(Of Int)
// which can safely accept concurrent add operations
query.ForAll((e) => concurrentBag.Add(Compute(e)));

다음 그림에서는 쿼리 실행과 관련하여 foreach와 ForAll<TSource> 간의 차이점을 보여 줍니다.

ForAll과 ForEach 비교

취소

PLINQ는 .NET Framework 4의 취소 형식과 통합되어 있습니다. 자세한 내용은 취소을 참조하십시오. 따라서 순차적 LINQ to Objects 쿼리와 달리 PLINQ 쿼리는 취소할 수 있습니다. 취소할 수 있는 PLINQ 쿼리를 만들려면 쿼리에 WithCancellation<TSource> 연산자를 사용하거나 CancellationToken 인스턴스를 인수로 제공합니다. 토큰의 IsCancellationRequested 속성이 true로 설정되어 있으면 PLINQ에서는 이를 인식하여 모든 스레드에서의 처리를 중지하고 OperationCanceledException을 throw합니다.

취소 토큰이 설정된 뒤에도 PLINQ 쿼리에서 일부 요소의 처리를 계속 진행할 수 있습니다.

응답성을 높이기 위해 장기 실행 사용자 대리자에서 취소 요청에 응답할 수도 있습니다. 자세한 내용은 방법: PLINQ 쿼리 취소를 참조하십시오.

예외

PLINQ 쿼리가 실행될 때 여러 예외가 서로 다른 스레드에서 동시에 throw될 수 있습니다. 또한 예외를 처리하는 코드와 예외를 throw한 코드가 서로 다른 스레드에 있을 수도 있습니다. PLINQ에서는 AggregateException 형식을 사용하여 쿼리에서 throw된 모든 예외를 캡슐화하고 이러한 예외를 호출 스레드로 다시 마샬링합니다. 호출 스레드에서는 하나의 try-catch 블록만 필요합니다. 그러나 AggregateException에 캡슐화된 모든 예외를 반복하고 안전하게 복원할 수 있는 예외를 catch할 수 있습니다. 드물기는 하지만 AggregateException에 래핑되지 않은 예외가 throw되는 경우도 있으며, ThreadAbortException도 래핑되지 않습니다.

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

자세한 내용은 방법: PLINQ 쿼리의 예외 처리를 참조하십시오.

사용자 지정 파티셔너

일부 경우 소스 데이터의 일부 특성을 사용하는 사용자 지정 파티셔너를 작성하여 쿼리 성능을 향상시킬 수 있습니다. 쿼리에서 사용자 지정 파티셔너는 그 자체가 쿼리되는 열거 가능한 개체입니다.

    [Visual Basic]
    Dim arr(10000) As Integer
    Dim partitioner = New MyArrayPartitioner(Of Integer)(arr)
    Dim query = partitioner.AsParallel().Select(Function(x) SomeFunction(x))
    [C#]
    int[] arr= ...;
    Partitioner<int> partitioner = newMyArrayPartitioner<int>(arr);
    var q = partitioner.AsParallel().Select(x => SomeFunction(x));

PLINQ에서는 고정된 수의 파티션을 지원합니다. 단, 런타임에는 부하 분산을 위해 해당 파티션에 데이터가 동적으로 다시 할당될 수도 있습니다. ForForEach에서는 동적 분할만 지원되므로 런타임에 파티션 수가 변경됩니다. 자세한 내용은 PLINQ 및 TPL에 대한 사용자 지정 파티셔너를 참조하십시오.

PLINQ 성능 측정

대부분의 경우 쿼리를 병렬화할 수 있지만 이때 얻을 수 있는 성능상의 이점보다는 병렬 쿼리를 설정하는 데 필요한 오버헤드를 우선적으로 고려해야 합니다. 쿼리에서 수행하는 계산 과정이 많지 않거나 데이터 소스가 작은 경우에는 PLINQ 쿼리가 순차적 LINQ to Objects 개체보다도 느릴 수 있습니다. Visual Studio Team Server의 병렬 성능 분석기를 사용하여 다양한 쿼리의 성능을 비교하고, 처리 병목 지점을 찾고, 쿼리를 병렬로 실행할지 순차적으로 실행할지를 결정할 수 있습니다. 자세한 내용은 동시성 시각화 도우미방법: PLINQ 쿼리 성능 측정을 참조하십시오.

참고 항목

개념

PLINQ(병렬 LINQ)

PLINQ의 속도 향상 이해