PLINQ 및 TPL에 대한 사용자 지정 파티셔너Custom Partitioners for PLINQ and TPL

데이터 소스에 대한 작업을 병렬화하기 위한 필수 단계 중 하나는 다중 스레드에서 동시에 액세스 가능한 여러 섹션으로 소스를 ‘분할’하는 것입니다. To parallelize an operation on a data source, one of the essential steps is to partition the source into multiple sections that can be accessed concurrently by multiple threads. PLINQ 및 TPL(작업 병렬 라이브러리)은 병렬 쿼리 또는 ForEach 루프를 쓸 때 투명하게 작동하는 기본 파티셔너를 제공합니다.PLINQ and the Task Parallel Library (TPL) provide default partitioners that work transparently when you write a parallel query or ForEach loop. 더 높은 수준의 시나리오에서는 고유한 파티셔너를 연결할 수 있습니다.For more advanced scenarios, you can plug in your own partitioner.

분할 유형Kinds of Partitioning

데이터 소스를 분할하는 여러 가지 방법이 있습니다.There are many ways to partition a data source. 가장 효율적인 방법에서는 소스를 여러 하위 시퀀스로 물리적으로 나누는 대신 여러 스레드가 공동 작업으로 원래 소스 시퀀스를 처리합니다.In the most efficient approaches, multiple threads cooperate to process the original source sequence, rather than physically separating the source into multiple subsequences. 미리 길이가 알려진 IList 컬렉션과 같은 배열 및 기타 인덱싱된 소스의 경우 ‘범위 분할’은 가장 간단한 분할 유형입니다. For arrays and other indexed sources such as IList collections where the length is known in advance, range partitioning is the simplest kind of partitioning. 모든 스레드는 고유한 시작 및 끝 인덱스를 수신하므로 다른 스레드가 덮어쓰거나 다른 스레드를 덮어쓰지 않고 소스의 해당 범위를 처리할 수 있습니다.Every thread receives unique beginning and ending indexes, so that it can process its range of the source without overwriting or being overwritten by any other thread. 범위 분할에 관련된 유일한 오버헤드는 범위를 만드는 초기 작업입니다. 그 이후에는 추가적인 동기화가 필요하지 않습니다.The only overhead involved in range partitioning is the initial work of creating the ranges; no additional synchronization is required after that. 따라서 워크로드가 균등하게 나누어지면 좋은 성능을 제공할 수 있습니다.Therefore, it can provide good performance as long as the workload is divided evenly. 범위 분할의 단점은 한 스레드가 조기에 완료되는 경우 다른 스레드가 작업을 완료하도록 도울 수 없다는 것입니다.A disadvantage of range partitioning is that if one thread finishes early, it cannot help the other threads finish their work.

연결된 목록 또는 길이가 알려지지 않은 기타 컬렉션의 경우 ‘청크 분할’을 사용할 수 있습니다. For linked lists or other collections whose length is not known, you can use chunk partitioning. 청크 분할에서 병렬 루프 또는 쿼리의 모든 스레드 또는 작업은 소스 요소 중 일부를 하나의 청크로 사용하여 처리한 다음, 돌아와서 추가 요소를 검색합니다.In chunk partitioning, every thread or task in a parallel loop or query consumes some number of source elements in one chunk, processes them, and then comes back to retrieve additional elements. 파티셔너는 모든 요소가 분배되고 중복이 없는지 확인합니다.The partitioner ensures that all elements are distributed and that there are no duplicates. 청크는 크기에 제한이 없습니다.A chunk may be any size. 예를 들어 방법: 동적 파티션 구현에 설명된 파티셔너는 하나의 요소만 포함하는 청크를 만듭니다.For example, the partitioner that is demonstrated in How to: Implement Dynamic Partitions creates chunks that contain just one element. 청크가 너무 크지 않다면 스레드에 대한 요소 할당이 미리 결정되지 않으므로 이 분할 유형은 기본적으로 부하 분산됩니다.As long as the chunks are not too large, this kind of partitioning is inherently load-balancing because the assignment of elements to threads is not pre-determined. 그러나 파티셔너는 스레드가 다른 청크를 가져와야 할 때마다 동기화 오버헤드를 발생시킵니다.However, the partitioner does incur the synchronization overhead each time the thread needs to get another chunk. 이러한 경우에 발생하는 동기화 양은 청크 크기에 반비례합니다.The amount of synchronization incurred in these cases is inversely proportional to the size of the chunks.

일반적으로 범위 분할은 대리자의 실행 시간이 짧거나 중간 정도이고 소스에 많은 요소가 포함된 경우에만 더 빠르고 각 파티션의 전체 작업은 거의 동일합니다.In general, range partitioning is only faster when the execution time of the delegate is small to moderate, and the source has a large number of elements, and the total work of each partition is roughly equivalent. 따라서 청크 분할은 일반적으로 대부분의 경우 더 빠릅니다.Chunk partitioning is therefore generally faster in most cases. 대리자에 대한 실행 시간이 더 길거나 요소 수가 적은 소스에서는 청크 및 범위 분할 성능이 거의 같습니다.On sources with a small number of elements or longer execution times for the delegate, then the performance of chunk and range partitioning is about equal.

또한 TPL 파티셔너는 동적 파티션 수를 지원합니다.The TPL partitioners also support a dynamic number of partitions. 즉, 이 파티셔너는 예를 들어 ForEach 루프가 새 작업을 생성할 경우 파티션을 즉시 만들 수 있습니다.This means they can create partitions on-the-fly, for example, when the ForEach loop spawns a new task. 이 기능을 사용하면 파티셔너가 루프 자체와 함께 확장될 할 수 있습니다.This feature enables the partitioner to scale together with the loop itself. 동적 파티셔너는 기본적으로 부하 분산됩니다.Dynamic partitioners are also inherently load-balancing. 사용자 지정 파티셔너를 만드는 경우 ForEach 루프에서 사용할 수 있도록 동적 분할을 지원해야 합니다.When you create a custom partitioner, you must support dynamic partitioning to be consumable from a ForEach loop.

PLINQ에 대한 부하 분산 파티셔너 구성Configuring Load Balancing Partitioners for PLINQ

Partitioner.Create 메서드의 일부 오버로드를 사용하면 배열 또는 IList 소스에 대한 파티셔너를 만들고 스레드 간에 워크로드를 분산하려고 시도할지 여부를 지정할 수 있습니다.Some overloads of the Partitioner.Create method let you create a partitioner for an array or IList source and specify whether it should attempt to balance the workload among the threads. 파티셔너가 부하 분산되도록 구성된 경우에는 청크 분할이 사용되고 요소가 요청 시 작은 청크로 각 파티션에 전달됩니다.When the partitioner is configured to load-balance, chunk partitioning is used, and the elements are handed off to each partition in small chunks as they are requested. 이 방법은 전체 루프 또는 쿼리가 완료될 때까지 모든 파티션에 처리할 요소가 있는지 확인하는 데 도움이 됩니다.This approach helps ensure that all partitions have elements to process until the entire loop or query is completed. 추가 오버로드를 사용하여 IEnumerable 소스의 부하 분산 분할을 제공할 수 있습니다.An additional overload can be used to provide load-balancing partitioning of any IEnumerable source.

일반적으로 부하 분산을 사용하려면 파티션이 파티셔너의 요소를 비교적 자주 요청해야 합니다.In general, load balancing requires the partitions to request elements relatively frequently from the partitioner. 반대로 정적 분할을 수행하는 파티셔너는 범위 또는 청크 분할을 사용하여 각 파티셔너에 요소를 한 번에 할당할 수 있습니다.By contrast, a partitioner that does static partitioning can assign the elements to each partitioner all at once by using either range or chunk partitioning. 이 작업에 필요한 오버헤드는 부하 분산보다 적지만 한 스레드가 다른 스레드보다 훨씬 더 많은 작업으로 종료될 경우 실행에 시간이 더 오래 걸릴 수 있습니다.This requires less overhead than load balancing, but it might take longer to execute if one thread ends up with significantly more work than the others. 기본적으로 IList 또는 배열을 전달할 때 PLINQ는 항상 부하 분산 없이 범위 분할을 사용합니다.By default when it is passed an IList or an array, PLINQ always uses range partitioning without load balancing. PLINQ에 대한 부하 분산을 사용하려면 다음 예제에 표시된 것처럼 Partitioner.Create 메서드를 사용합니다.To enable load balancing for PLINQ, use the Partitioner.Create method, as shown in the following example.

// Static partitioning requires indexable source. Load balancing
// can use any IEnumerable.
var nums = Enumerable.Range(0, 100000000).ToArray();

// Create a load-balancing partitioner. Or specify false for static partitioning.
Partitioner<int> customPartitioner = Partitioner.Create(nums, true);

// The partitioner is the query's data source.
var q = from x in customPartitioner.AsParallel()
        select x * Math.PI;

q.ForAll((x) =>
{
    ProcessData(x);
});
' Static number of partitions requires indexable source.
Dim nums = Enumerable.Range(0, 100000000).ToArray()

' Create a load-balancing partitioner. Or specify false For  Shared partitioning.
Dim customPartitioner = Partitioner.Create(nums, True)

' The partitioner is the query's data source.
Dim q = From x In customPartitioner.AsParallel()
        Select x * Math.PI

q.ForAll(Sub(x) ProcessData(x))

해당 시나리오에서 부하 분산을 사용할지 여부를 결정하려면 대표적인 부하 및 컴퓨터 구성에서 작업 완료에 걸리는 시간을 실제로 실험 및 측정하는 것이 가장 좋습니다.The best way to determine whether to use load balancing in any given scenario is to experiment and measure how long it takes operations to complete under representative loads and computer configurations. 예를 들어 정적 분할은 몇 개의 코어만 있는 멀티 코어 컴퓨터에서 상당한 속도 향상을 제공할 수 있지만, 비교적 많은 코어가 있는 컴퓨터에서는 속도 저하가 발생할 수 있습니다.For example, static partitioning might provide significant speedup on a multi-core computer that has only a few cores, but it might result in slowdowns on computers that have relatively many cores.

다음 표에는 Create 메서드의 사용 가능한 오버로드가 나와 있습니다.The following table lists the available overloads of the Create method. 이 파티셔너는 PLINQ 또는 Task와 함께 사용하도록 제한되지 않습니다.These partitioners are not limited to use only with PLINQ or Task. 사용자 지정 병렬 구문과 함께 사용할 수도 있습니다.They can also be used with any custom parallel construct.

오버로드Overload 부하 분산 사용Uses load balancing
Create<TSource>(IEnumerable<TSource>) 항상Always
Create<TSource>(TSource[], Boolean) 부울 인수가 true로 지정될 경우When the Boolean argument is specified as true
Create<TSource>(IList<TSource>, Boolean) 부울 인수가 true로 지정될 경우When the Boolean argument is specified as true
Create(Int32, Int32) NeverNever
Create(Int32, Int32, Int32) NeverNever
Create(Int64, Int64) NeverNever
Create(Int64, Int64, Int64) NeverNever

Parallel.ForEach에 대한 정적 범위 파티셔너 구성Configuring Static Range Partitioners for Parallel.ForEach

For 루프에서 루프 본문은 메서드에 대리자로 제공됩니다.In a For loop, the body of the loop is provided to the method as a delegate. 해당 대리자를 호출하는 비용은 가상 메서드 호출과 거의 같습니다.The cost of invoking that delegate is about the same as a virtual method call. 일부 시나리오에서 병렬 루프의 본문은 각 루프 반복에 대한 대리자 호출 비용이 매우 커질 만큼 충분히 작을 수 있습니다.In some scenarios, the body of a parallel loop might be small enough that the cost of the delegate invocation on each loop iteration becomes significant. 이 경우 Create 오버로드 중 하나를 사용하여 소스 요소에 대해 범위 분할의 IEnumerable<T>을 만들 수 있습니다.In such situations, you can use one of the Create overloads to create an IEnumerable<T> of range partitions over the source elements. 그런 다음, 본문이 일반 for 루프로 구성된 ForEach 메서드에 이 범위 컬렉션을 전달할 수 있습니다.Then, you can pass this collection of ranges to a ForEach method whose body consists of a regular for loop. 이 방법의 장점은 대리자 호출 비용이 요소당 한 번이 아니라 범위당 한 번만 발생한다는 점입니다.The benefit of this approach is that the delegate invocation cost is incurred only once per range, rather than once per element. 다음 예제에서는 기본 패턴을 보여줍니다.The following example demonstrates the basic pattern.

using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {

        // Source must be array or IList.
        var source = Enumerable.Range(0, 100000).ToArray();

        // Partition the entire source array.
        var rangePartitioner = Partitioner.Create(0, source.Length);

        double[] results = new double[source.Length];

        // Loop over the partitions in parallel.
        Parallel.ForEach(rangePartitioner, (range, loopState) =>
        {
            // Loop over each range element without a delegate invocation.
            for (int i = range.Item1; i < range.Item2; i++)
            {
                results[i] = source[i] * Math.PI;
            }
        });

        Console.WriteLine("Operation complete. Print results? y/n");
        char input = Console.ReadKey().KeyChar;
        if (input == 'y' || input == 'Y')
        {
            foreach(double d in results)
            {
                Console.Write("{0} ", d);
            }           
        }
    }
}
Imports System.Threading.Tasks
Imports System.Collections.Concurrent

Module PartitionDemo

    Sub Main()
        ' Source must be array or IList.
        Dim source = Enumerable.Range(0, 100000).ToArray()

        ' Partition the entire source array. 
        ' Let the partitioner size the ranges.
        Dim rangePartitioner = Partitioner.Create(0, source.Length)

        Dim results(source.Length - 1) As Double

        ' Loop over the partitions in parallel. The Sub is invoked
        ' once per partition.
        Parallel.ForEach(rangePartitioner, Sub(range, loopState)

                                               ' Loop over each range element without a delegate invocation.
                                               For i As Integer = range.Item1 To range.Item2 - 1
                                                   results(i) = source(i) * Math.PI
                                               Next
                                           End Sub)
        Console.WriteLine("Operation complete. Print results? y/n")
        Dim input As Char = Console.ReadKey().KeyChar
        If input = "y"c Or input = "Y"c Then
            For Each d As Double In results
                Console.Write("{0} ", d)
            Next
        End If

    End Sub
End Module

루프의 모든 스레드는 지정된 하위 범위의 시작 및 끝 인덱스 값을 포함하는 자체 Tuple<T1,T2>을 수신합니다.Every thread in the loop receives its own Tuple<T1,T2> that contains the starting and ending index values in the specified sub-range. 내부 for 루프는 fromInclusivetoExclusive 값을 사용하여 배열 또는 IList를 반복합니다.The inner for loop uses the fromInclusive and toExclusive values to loop over the array or the IList directly.

Create 오버로드 중 하나를 사용하여 파티션 크기와 파티션 수를 지정할 수 있습니다.One of the Create overloads lets you specify the size of the partitions, and the number of partitions. 이 오버로드는 요소당 가상 메서드 호출이 하나라도 성능에 큰 영향을 미치도록 요소당 작업이 느린 시나리오에서 사용할 수 있습니다.This overload can be used in scenarios where the work per element is so low that even one virtual method call per element has a noticeable impact on performance.

사용자 지정 파티셔너Custom Partitioners

일부 시나리오에서는 이것이 고유한 파티셔너를 구현하는 데 유용하거나 필요할 수도 있습니다.In some scenarios, it might be worthwhile or even required to implement your own partitioner. 예를 들어 클래스의 내부 구조에 대한 지식을 바탕으로 기본 파티셔너가 분할할 수 있는 것보다 더 효율적으로 분할할 수 있는 사용자 지정 컬렉션 클래스가 있을 수 있습니다.For example, you might have a custom collection class that you can partition more efficiently than the default partitioners can, based on your knowledge of the internal structure of the class. 또는 소스 컬렉션의 여러 위치에서 요소를 처리하는 데 걸리는 시간에 대한 지식을 바탕으로 다양한 크기의 범위 파티션을 만들 수 있습니다.Or, you may want to create range partitions of varying sizes based on your knowledge of how long it will take to process elements at different locations in the source collection.

기본 사용자 지정 파티셔너를 만들려면 System.Collections.Concurrent.Partitioner<TSource>에서 클래스를 파생시키고 다음 표에 설명된 대로 가상 메서드를 재정의합니다.To create a basic custom partitioner, derive a class from System.Collections.Concurrent.Partitioner<TSource> and override the virtual methods, as described in the following table.

GetPartitions 이 메서드는 기본 스레드에 의해 한 번 호출되고 IList(IEnumerator(TSource))를 반환합니다.This method is called once by the main thread and returns an IList(IEnumerator(TSource)). 루프 또는 쿼리의 각 작업자 스레드는 목록에서 GetEnumerator를 호출하여 별도의 파티션에서 IEnumerator<T>를 검색할 수 있습니다.Each worker thread in the loop or query can call GetEnumerator on the list to retrieve a IEnumerator<T> over a distinct partition.
SupportsDynamicPartitions GetDynamicPartitions를 구현하는 경우 true를 반환하고, 그렇지 않으면 false를 반환합니다.Return true if you implement GetDynamicPartitions, otherwise, false.
GetDynamicPartitions SupportsDynamicPartitionstrue이면 선택적으로 GetPartitions 대신 이 메서드가 호출될 수 있습니다.If SupportsDynamicPartitions is true, this method can optionally be called instead of GetPartitions.

결과가 정렬 가능해야 하거나 요소에 대한 인덱싱된 액세스가 필요한 경우 System.Collections.Concurrent.OrderablePartitioner<TSource>에서 파생시키고 다음 표에 설명된 대로 해당 가상 메서드를 재정의합니다.If the results must be sortable or you require indexed access into the elements, then derive from System.Collections.Concurrent.OrderablePartitioner<TSource> and override its virtual methods as described in the following table.

GetPartitions 이 메서드는 기본 스레드에 의해 한 번 호출되고 IList(IEnumerator(TSource))를 반환합니다.This method is called once by the main thread and returns an IList(IEnumerator(TSource)). 루프 또는 쿼리의 각 작업자 스레드는 목록에서 GetEnumerator를 호출하여 별도의 파티션에서 IEnumerator<T>를 검색할 수 있습니다.Each worker thread in the loop or query can call GetEnumerator on the list to retrieve a IEnumerator<T> over a distinct partition.
SupportsDynamicPartitions GetDynamicPartitions를 구현하는 경우 true를 반환하고, 그렇지 않으면 false를 반환합니다.Return true if you implement GetDynamicPartitions; otherwise, false.
GetDynamicPartitions 일반적으로 GetOrderableDynamicPartitions만 호출합니다.Typically, this just calls GetOrderableDynamicPartitions.
GetOrderableDynamicPartitions SupportsDynamicPartitionstrue이면 선택적으로 GetPartitions 대신 이 메서드가 호출될 수 있습니다.If SupportsDynamicPartitions is true, this method can optionally be called instead of GetPartitions.

다음 표에서는 세 종류의 부하 분산 파티셔너가 OrderablePartitioner<TSource> 클래스를 구현하는 방법을 추가로 자세히 설명합니다.The following table provides additional details about how the three kinds of load-balancing partitioners implement the OrderablePartitioner<TSource> class.

메서드/속성Method/Property 부하 분산 없는 IList/배열IList / Array without Load Balancing 부하 분산 있는 IList/배열IList / Array with Load Balancing IEnumerableIEnumerable
GetOrderablePartitions 범위 분할을 사용합니다.Uses range partitioning 지정된 partitionCount에 대해 최적화된 청크 분할을 사용합니다.Uses chunk partitioning optimized for Lists for the partitionCount specified 정적 개수의 파티션을 만들어 청크 분할을 사용합니다.Uses chunk partitioning by creating a static number of partitions.
OrderablePartitioner<TSource>.GetOrderableDynamicPartitions 지원되지 않는 예외를 throw합니다.Throws not-supported exception 목록 및 동적 파티션에 최적화된 청크 분할을 사용합니다.Uses chunk partitioning optimized for Lists and dynamic partitions 동적 개수의 파티션을 만들어 청크 분할을 사용합니다.Uses chunk partitioning by creating a dynamic number of partitions.
KeysOrderedInEachPartition true를 반환합니다.Returns true true를 반환합니다.Returns true true를 반환합니다.Returns true
KeysOrderedAcrossPartitions true를 반환합니다.Returns true false를 반환합니다.Returns false false를 반환합니다.Returns false
KeysNormalized true를 반환합니다.Returns true true를 반환합니다.Returns true true를 반환합니다.Returns true
SupportsDynamicPartitions false를 반환합니다.Returns false true를 반환합니다.Returns true true를 반환합니다.Returns true

동적 파티션Dynamic Partitions

ForEach 메서드에서 파티셔너를 사용하도록 하려면 동적 개수의 파티션을 반환할 수 있어야 합니다.If you intend the partitioner to be used in a ForEach method, you must be able to return a dynamic number of partitions. 즉, 파티셔너는 루프 실행 중에 언제든지 요청 시 새 파티션에 대한 열거자를 제공할 수 있습니다.This means that the partitioner can supply an enumerator for a new partition on-demand at any time during loop execution. 기본적으로 루프는 새 병렬 작업을 추가할 때마다 해당 작업에 대한 새 파티션을 요청합니다.Basically, whenever the loop adds a new parallel task, it requests a new partition for that task. 데이터의 순서가 지정 가능해야 하는 경우 각 파티션의 각 항목에 고유한 인덱스가 할당되도록 System.Collections.Concurrent.OrderablePartitioner<TSource>에서 파생시킵니다.If you require the data to be orderable, then derive from System.Collections.Concurrent.OrderablePartitioner<TSource> so that each item in each partition is assigned a unique index.

자세한 내용과 예제는 방법: 동적 파티션 구현을 참조하세요.For more information, and an example, see How to: Implement Dynamic Partitions.

파티셔너에 대한 계약Contract for Partitioners

사용자 지정 파티셔너를 구현할 경우 다음 지침에 따르면 TPL에서 PLINQ 및 ForEach를 올바르게 조작할 수 있습니다.When you implement a custom partitioner, follow these guidelines to help ensure correct interaction with PLINQ and ForEach in the TPL:

  • GetPartitionspartitionsCount에 대해 0보다 작거나 같은 인수를 사용하여 호출되면 ArgumentOutOfRangeException을 throw합니다.If GetPartitions is called with an argument of zero or less for partitionsCount, throw ArgumentOutOfRangeException. PLINQ 및 TPL이 0과 같은 partitionCount로는 전달되지 않더라도 가능성을 방지하는 것이 좋습니다.Although PLINQ and TPL will never pass in a partitionCount equal to 0, we nevertheless recommend that you guard against the possibility.

  • GetPartitionsGetOrderablePartitions는 항상 partitionsCount개의 파티션을 반환해야 합니다.GetPartitions and GetOrderablePartitions should always return partitionsCount number of partitions. 파티셔너가 데이터를 모두 사용해서 요청한 만큼 파티션을 만들 수 없는 경우 메서드는 나머지 파티션에 대해 각각 빈 열거자를 반환해야 합니다.If the partitioner runs out of data and cannot create as many partitions as requested, then the method should return an empty enumerator for each of the remaining partitions. 그렇지 않으면 PLINQ 및 TPL이 둘 다 InvalidOperationException을 throw합니다.Otherwise, both PLINQ and TPL will throw an InvalidOperationException.

  • GetPartitions, GetOrderablePartitions, GetDynamicPartitionsGetOrderableDynamicPartitionsnull(Visual Basic의 Nothing)을 반환하면 안 됩니다.GetPartitions, GetOrderablePartitions, GetDynamicPartitions, and GetOrderableDynamicPartitions should never return null (Nothing in Visual Basic). 반환할 경우 PLINQ/TPL이 InvalidOperationException을 throw합니다.If they do, PLINQ / TPL will throw an InvalidOperationException.

  • 파티션을 반환하는 메서드는 항상 데이터 소스를 완전히 고유하게 열거할 수 있는 파티션을 반환해야 합니다.Methods that return partitions should always return partitions that can fully and uniquely enumerate the data source. 파티셔너의 디자인에 특별히 필요한 경우가 아니면 데이터 소스 또는 건너뛴 항목에는 중복이 없어야 합니다.There should be no duplication in the data source or skipped items unless specifically required by the design of the partitioner. 이 규칙을 따르지 않으면 출력 순서가 변경될 수 있습니다.If this rule is not followed, then the output order may be scrambled.

  • 다음 부울 getter는 출력 순서가 변경되지 않도록 항상 다음 값을 정확하게 반환해야 합니다.The following Boolean getters must always accurately return the following values so that the output order is not scrambled:

    • KeysOrderedInEachPartition: 각 파티션이 키 인덱스가 증가하는 요소를 반환합니다.KeysOrderedInEachPartition: Each partition returns elements with increasing key indices.

    • KeysOrderedAcrossPartitions: 반환되는 모든 파티션에서 파티션의 키 인덱스 i는 파티션의 키 인덱스 i-1보다 큽니다.KeysOrderedAcrossPartitions: For all partitions that are returned, the key indices in partition i are higher than the key indices in partition i-1.

    • KeysNormalized: 모든 키 인덱스는 0부터 시작하여 공백 없이 순차적으로 증가합니다.KeysNormalized: All key indices are monotonically increasing without gaps, starting from zero.

  • 모든 인덱스는 고유해야 합니다.All indices must be unique. 중복 인덱스가 없어야 합니다.There may not be duplicate indices. 이 규칙을 따르지 않으면 출력 순서가 변경될 수 있습니다.If this rule is not followed, then the output order may be scrambled.

  • 모든 인덱스는 음수가 아니어야 합니다.All indices must be nonnegative. 이 규칙을 따르지 않으면 PLINQ/TPL이 예외를 throw할 수 있습니다.If this rule is not followed, then PLINQ/TPL may throw exceptions.

참고 항목See also