다음을 통해 공유


PLINQ 소개

PLINQ(병렬 LINQ)는 LINQ(Language-Integrated Query) 패턴의 병렬 구현입니다. PLINQ는 LINQ 표준 쿼리 연산자의 전체 집합을 네임스페이스의 확장 메서드 System.Linq 로 구현하고 병렬 작업에 대한 추가 연산자를 제공합니다. PLINQ는 LINQ 구문의 단순성과 가독성을 병렬 프로그래밍의 기능과 결합합니다.

팁 (조언)

LINQ에 익숙하지 않은 경우 형식 안전 방식으로 열거 가능한 데이터 원본을 쿼리하기 위한 통합 모델을 제공합니다. LINQ to Objects는 메모리 내 컬렉션(예: List<T> 배열)에 대해 실행되는 LINQ 쿼리의 이름입니다. 이 문서에서는 LINQ에 대한 기본적인 이해가 있다고 가정합니다. 자세한 내용은 Language-Integrated 쿼리(LINQ)를 참조하세요.

병렬 쿼리란?

PLINQ 쿼리는 여러 가지 면에서 비 병렬 LINQ to Objects 쿼리와 유사합니다. 순차 LINQ 쿼리와 마찬가지로 PLINQ 쿼리는 메모리 IEnumerable 내 또는 IEnumerable<T> 데이터 원본에서 작동하며 실행이 지연되어 쿼리가 열거될 때까지 실행을 시작하지 않습니다. 주요 차이점은 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 소개(C#)LINQ 소개(Visual Basic)를 참조하세요.

표준 쿼리 연산 ParallelEnumerable 자 외에도 클래스에는 병렬 실행과 관련된 동작을 사용하도록 설정하는 메서드 집합이 포함되어 있습니다. 이러한 PLINQ 관련 메서드는 다음 표에 나와 있습니다.

ParallelEnumerable 연산자 설명
AsParallel PLINQ의 진입점입니다. 가능한 경우 나머지 쿼리를 병렬 처리되도록 지정합니다.
AsSequential 나머지 쿼리는 병렬이 아닌 LINQ 쿼리로 순차적으로 실행되도록 지정합니다.
AsOrdered PLINQ가 쿼리의 나머지 부분에 대한 소스 시퀀스의 순서를 유지하거나 순서가 변경될 때까지(예: orderby(Visual Basic의 Order By) 절 사용) 순서를 유지하도록 지정합니다.
AsUnordered 원본 시퀀스의 순서를 유지하기 위해 나머지 쿼리에 대한 PLINQ가 필요하지 않도록 지정합니다.
WithCancellation PLINQ가 제공된 취소 토큰의 상태를 주기적으로 모니터링하고 요청된 경우 실행을 취소하도록 지정합니다.
WithDegreeOfParallelism PLINQ가 쿼리를 병렬화하는 데 사용해야 하는 최대 프로세서 수를 지정합니다.
WithMergeOptions PLINQ가 가능한 경우 병렬 결과를 사용 중인 스레드에서 하나의 시퀀스로 병합하는 방법에 대한 힌트를 제공합니다.
WithExecutionMode 기본 동작이 순차적으로 실행되는 경우에도 PLINQ가 쿼리를 병렬 처리해야 하는지 여부를 지정합니다.
ForAll 쿼리 결과를 반복하는 것과 달리 먼저 소비자 스레드에 다시 병합하지 않고도 결과를 병렬로 처리할 수 있는 다중 스레드 열거형 메서드입니다.
Aggregate 과부하 PLINQ에 고유하고 스레드 로컬 파티션에 대한 중간 집계를 가능하게 하는 오버로드와 모든 파티션의 결과를 결합하는 최종 집계 함수입니다.

옵트인 모델

쿼리를 작성할 때 다음 예제와 같이 데이터 원본에서 확장 메서드를 호출하여 ParallelEnumerable.AsParallel PLINQ에 옵트인합니다.

var source = Enumerable.Range(1, 10000);

// Opt in to PLINQ with AsParallel.
var evenNums = from num in source.AsParallel()
               where num % 2 == 0
               select num;
Console.WriteLine($"{evenNums.Count()} even numbers out of {source.Count()} total");
// The example displays the following output:
//       5000 even numbers out of 10000 total
Dim source = Enumerable.Range(1, 10000)

' Opt in to PLINQ with AsParallel
Dim evenNums = From num In source.AsParallel()
               Where num Mod 2 = 0
               Select num
Console.WriteLine("{0} even numbers out of {1} total",
                  evenNums.Count(), source.Count())
' The example displays the following output:
'       5000 even numbers out of 10000 total

확장 메서드는 AsParallel 이후의 쿼리 연산자들, 즉 이 경우에는 whereselectSystem.Linq.ParallelEnumerable 구현에 연결합니다.

실행 모드

기본적으로 PLINQ는 보수적입니다. 런타임에 PLINQ 인프라는 쿼리의 전체 구조를 분석합니다. 쿼리가 병렬화로 속도 향상을 생성할 가능성이 있는 경우 PLINQ는 소스 시퀀스를 동시에 실행할 수 있는 작업으로 분할합니다. 쿼리를 병렬화하는 것이 안전하지 않은 경우 PLINQ는 쿼리를 순차적으로 실행합니다. PLINQ가 잠재적으로 비용이 많이 드는 병렬 알고리즘 또는 저렴한 순차 알고리즘 중에서 선택할 수 있는 경우 기본적으로 순차 알고리즘을 선택합니다. PLINQ에 병렬 알고리즘을 선택하도록 지시하기 위해 WithExecutionMode 메서드와 System.Linq.ParallelExecutionMode 열거형을 사용할 수 있습니다. 이는 특정 쿼리가 병렬로 더 빠르게 실행되는지 테스트 및 측정하여 알고 있는 경우에 유용합니다. 자세한 내용은 방법: PLINQ에서 실행 모드 지정을 참조하세요.

병렬 처리 수준

기본적으로 PLINQ는 호스트 컴퓨터의 모든 프로세서를 사용합니다. PLINQ에 WithDegreeOfParallelism 메서드를 사용하여 지정된 수 이상의 프로세서를 사용하지 않도록 지시할 수 있습니다. 이 기능은 컴퓨터에서 실행되는 다른 프로세스가 특정 CPU 시간을 수신하도록 하려는 경우에 유용합니다. 다음 코드 조각은 쿼리를 최대 2개의 프로세서를 사용하도록 제한합니다.

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

쿼리가 파일 I/O와 같이 컴퓨팅에 바인딩되지 않은 상당한 양의 작업을 수행하는 경우 컴퓨터의 코어 수보다 큰 병렬 처리 수준을 지정하는 것이 좋습니다.

정렬된 쿼리 및 정렬되지 않은 병렬 쿼리

일부 쿼리에서 쿼리 연산자는 원본 시퀀스의 순서를 유지하는 결과를 생성해야 합니다. PLINQ는 이 목적을 위해 연산자를 AsOrdered 제공합니다. AsOrderedAsSequential와(과) 구분됩니다. AsOrdered 시퀀스는 여전히 병렬로 처리되지만 결과는 버퍼링되고 정렬됩니다. 순서 보존에는 일반적으로 추가 작업이 AsOrdered 포함되므로 시퀀스가 기본 AsUnordered 시퀀스보다 더 느리게 처리될 수 있습니다. 특정 순서가 지정된 병렬 작업이 순차적 작업 버전보다 빠른지 여부는 여러 요인에 따라 달라집니다.

다음 코드 예제에서는 순서 보존에 가입하는 방법을 보여 줍니다.

var evenNums =
    from num in numbers.AsParallel().AsOrdered()
    where num % 2 == 0
    select num;
Dim evenNums = From num In numbers.AsParallel().AsOrdered()
               Where num Mod 2 = 0
               Select num


자세한 내용은 PLINQ의 Order 유지를 참조하세요.

병렬 쿼리와 순차 쿼리 비교

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

쿼리 결과 병합 옵션

PLINQ 쿼리가 병렬로 실행되면 각 작업자 스레드의 결과를 foreach 루프(For Each Visual Basic), 또는 목록이나 배열에 삽입하기 위해 주 스레드로 다시 병합해야 합니다. 경우에 따라 결과 생성을 더 빨리 시작하는 등의 특정 종류의 병합 작업을 지정하는 것이 도움이 될 수 있습니다. 이를 위해 PLINQ는 WithMergeOptions 메서드 및 열거형을 ParallelMergeOptions 지원합니다. 자세한 내용은 PLINQ의 병합 옵션을 참조하세요.

ForAll 연산자

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

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)));
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)))

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

ForAll 대 ForEach

취소

PLINQ는 .NET의 취소 유형과 통합됩니다. (자세한 내용은 관리되는 스레드의 취소를 참조하세요.) 따라서 순차 LINQ to Objects 쿼리와 달리 PLINQ 쿼리를 취소할 수 있습니다. 취소 가능한 PLINQ 쿼리를 만들려면 쿼리에서 연산자를 WithCancellation 사용하고 인스턴스를 CancellationToken 인수로 제공합니다. 토큰의 IsCancellationRequested 속성이 true로 설정되면, PLINQ가 이를 감지하고 모든 스레드에서 처리를 중단한 후 OperationCanceledException를 throw합니다.

취소 토큰이 설정된 후에도 PLINQ 쿼리가 일부 요소를 계속 처리할 수 있습니다.

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

예외

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

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

자세한 내용은 방법: PLINQ 쿼리에서 예외 처리를 참조하세요.

사용자 지정 파티셔너

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

int[] arr = new int[9999];
Partitioner<int> partitioner = new MyArrayPartitioner<int>(arr);
var query = partitioner.AsParallel().Select(SomeFunction);
Dim arr(10000) As Integer
Dim partitioner As Partitioner(Of Integer) = New MyArrayPartitioner(Of Integer)(arr)
Dim query = partitioner.AsParallel().Select(Function(x) SomeFunction(x))

PLINQ는 고정된 수의 파티션을 지원합니다(부하 분산을 위해 런타임 동안 데이터가 해당 파티션에 동적으로 다시 할당될 수 있음). For 동적 ForEach 분할만 지원합니다. 즉, 런타임에 파티션 수가 변경됩니다. 자세한 내용은 PLINQ 및 TPL에 대한 사용자 지정 파티셔너를 참조하세요.

PLINQ 성능 측정

대부분의 경우 쿼리를 병렬 처리할 수 있지만 병렬 쿼리를 설정하는 오버헤드는 얻은 성능 이점보다 큽니다. 쿼리가 많은 계산을 수행하지 않거나 데이터 원본이 작은 경우 PLINQ 쿼리가 순차 LINQ to Objects 쿼리보다 느려질 수 있습니다. Visual Studio Team Server의 병렬 성능 분석기를 사용하여 다양한 쿼리의 성능을 비교하고, 처리 병목 상태를 찾고, 쿼리가 병렬 또는 순차적으로 실행되고 있는지 확인할 수 있습니다. 자세한 내용은 동시성 시각화 도우미방법: PLINQ 쿼리 성능 측정을 참조하세요.

참고하십시오