PLINQ ve TPL için Özel Bölümleyiciler
Veri kaynağındaki bir işlemi paralelleştirmek için, temel adımlardan biri kaynağı birden çok iş parçacığı tarafından eşzamanlı olarak erişilebilen birden çok bölüme bölmektir . PLINQ ve Görev Paralel Kitaplığı (TPL), paralel sorgu veya ForEach döngü yazdığınızda saydam olarak çalışan varsayılan bölümleyiciler sağlar. Daha gelişmiş senaryolar için kendi bölümleyicinizi takabilirsiniz.
Bölümleme Türleri
Veri kaynağını bölümlemenin birçok yolu vardır. En verimli yaklaşımlarda, birden çok iş parçacığı kaynağı fiziksel olarak birden çok alt diziye ayırmak yerine özgün kaynak dizisini işlemek için işbirliği sağlar. Uzunluğu önceden bilinen koleksiyonlar gibi IList diziler ve diğer dizine alınan kaynaklar için, aralık bölümleme en basit bölümleme türüdür. Her iş parçacığı benzersiz başlangıç ve bitiş dizinleri alır, böylece başka bir iş parçacığının üzerine yazmadan veya üzerine yazılmadan kaynak aralığını işleyebilir. Aralık bölümlemedeki tek ek yük, aralıkları oluşturmanın ilk çalışmasıdır; bundan sonra ek eşitleme gerekmez. Bu nedenle, iş yükü eşit olarak bölündüğü sürece iyi bir performans sağlayabilir. Aralık bölümlemenin bir dezavantajı, bir iş parçacığı erken biterse diğer iş parçacıklarının işlerini bitirmesine yardımcı olamayacağıdır.
Uzunluğu bilinmeyen bağlantılı listeler veya diğer koleksiyonlar için öbek bölümlemesi kullanabilirsiniz. Öbek bölümlemede, paralel döngüdeki veya sorgudaki her iş parçacığı veya görev bir öbekte bazı kaynak öğeleri kullanır, işler ve sonra ek öğeleri almak için geri gelir. Bölümleyici tüm öğelerin dağıtılmasını ve yineleme olmamasını sağlar. Öbek herhangi bir boyutta olabilir. Örneğin, Nasıl yapılır: Dinamik Bölümleri Uygulama bölümünde görüntülenen bölümleyici, yalnızca bir öğe içeren öbekler oluşturur. Öbekler çok büyük olmadığı sürece, öğelerin iş parçacıklarına atanması önceden belirlenmediğinden bu tür bir bölümleme doğal olarak yük dengelemedir. Ancak, iş parçacığının başka bir öbek alması gerektiğinde bölümleyici eşitleme ek yüküne neden olur. Bu durumlarda tahakkuk eden eşitleme miktarı öbeklerin boyutuyla ters orantılıdır.
Genel olarak, aralık bölümleme yalnızca temsilcinin yürütme süresi küçük ve orta olduğunda daha hızlıdır ve kaynakta çok sayıda öğe vardır ve her bölümün toplam çalışması kabaca eşdeğerdir. Bu nedenle öbek bölümleme genellikle çoğu durumda daha hızlıdır. Temsilci için az sayıda öğeye veya daha uzun yürütme sürelerine sahip kaynaklarda öbek ve aralık bölümlemesine ilişkin performans yaklaşık eşittir.
TPL bölümleyicileri dinamik sayıda bölümü de destekler. Bu, örneğin döngü yeni bir görev oluşturduğunda ForEach anında bölümler oluşturabilecekleri anlamına gelir. Bu özellik bölümleyicinin döngünün kendisiyle birlikte ölçeklendirilmesini sağlar. Dinamik bölümleyiciler de doğal olarak yük dengelemedir. Özel bölümleyici oluşturduğunuzda, dinamik bölümlemenin bir ForEach döngüden kullanılabilir olmasını desteklemeniz gerekir.
PLINQ için Yük Dengeleme Bölümleyicilerini Yapılandırma
yönteminin Partitioner.Create bazı aşırı yüklemeleri, bir dizi veya IList kaynak için bölümleyici oluşturmanıza ve iş yüklerini iş parçacıkları arasında dengelemeye çalışıp çalışmayacağını belirtmenize olanak sağlar. Bölümleyici yük dengelemesi için yapılandırıldığında, öbek bölümleme kullanılır ve öğeler istendiği gibi küçük parçalar halinde her bölüme devredilir. Bu yaklaşım, döngünün veya sorgunun tamamı tamamlanana kadar tüm bölümlerin işlenmek üzere öğeleri olduğundan emin olmanıza yardımcı olur. Herhangi IEnumerable bir kaynağın yük dengeleme bölümlemesini sağlamak için ek bir aşırı yükleme kullanılabilir.
Genel olarak yük dengeleme, bölümlerin bölümleyiciden nispeten sık öğe istemesini gerektirir. Buna karşılık, statik bölümleme kullanan bir bölümleyici, aralık veya öbek bölümlemesi kullanarak öğeleri her bölümleyiciye aynı anda atayabilir. Bu, yük dengelemeden daha az ek yük gerektirir, ancak bir iş parçacığının diğerlerinden önemli ölçüde daha fazla çalışmayla sonuçlanması durumunda yürütülmesi daha uzun sürebilir. Varsayılan olarak, bir IList veya dizi geçirildiğinde PLINQ her zaman yük dengeleme olmadan aralık bölümleme kullanır. PLINQ için yük dengelemeyi etkinleştirmek için aşağıdaki örnekte gösterildiği gibi yöntemini kullanın Partitioner.Create
.
// 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))
Belirli bir senaryoda yük dengelemenin kullanılıp kullanılmayacağını belirlemenin en iyi yolu, temsili yükler ve bilgisayar yapılandırmaları altında işlemlerin tamamlanmasının ne kadar sürdüğünü denemek ve ölçmektir. Örneğin, statik bölümleme yalnızca birkaç çekirdeği olan çok çekirdekli bir bilgisayarda önemli bir hız sağlayabilir, ancak nispeten çok sayıda çekirdeği olan bilgisayarlarda yavaşlamalara neden olabilir.
Aşağıdaki tabloda yönteminin kullanılabilir aşırı yüklemeleri Create listelenir. Bu bölümleyiciler yalnızca PLINQ veya Taskile kullanımla sınırlı değildir. Ayrıca herhangi bir özel paralel yapıyla da kullanılabilirler.
Aşırı | Yük dengelemeyi kullanır |
---|---|
Create<TSource>(IEnumerable<TSource>) | Her zaman |
Create<TSource>(TSource[], Boolean) | Boole bağımsız değişkeni true olarak belirtildiğinde |
Create<TSource>(IList<TSource>, Boolean) | Boole bağımsız değişkeni true olarak belirtildiğinde |
Create(Int32, Int32) | Asla |
Create(Int32, Int32, Int32) | Asla |
Create(Int64, Int64) | Asla |
Create(Int64, Int64, Int64) | Asla |
Parallel.ForEach için Statik Aralık Bölümleyicilerini Yapılandırma
Bir For döngüde, döngünün gövdesi yönteme temsilci olarak sağlanır. Bu temsilciyi çağırmanın maliyeti sanal yöntem çağrısıyla yaklaşık aynıdır. Bazı senaryolarda, paralel döngünün gövdesi, her döngü yinelemesindeki temsilci çağırma maliyetinin önemli hale gelmesi için yeterince küçük olabilir. Böyle durumlarda, kaynak öğeler üzerinde aralık bölümlerinden Create birini oluşturmak IEnumerable<T> için aşırı yüklemelerden birini kullanabilirsiniz. Ardından, bu aralık koleksiyonunu gövdesi normal for
bir döngüden oluşan bir ForEach yönteme geçirebilirsiniz. Bu yaklaşımın avantajı, temsilci çağırma maliyetinin öğe başına bir kez değil aralık başına yalnızca bir kez tahakkuk ettirilmesidir. Aşağıdaki örnekte temel desen gösterilmektedir.
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
Döngüdeki her iş parçacığı, belirtilen alt aralıktaki başlangıç ve bitiş dizini değerlerini içeren kendi Tuple<T1,T2> iş parçacığını alır. İç for
döngü, dizi veya IList doğrudan döngü yapmak için ve toExclusive
değerlerini kullanırfromInclusive
.
Aşırı yüklemelerden Create biri, bölümlerin boyutunu ve bölüm sayısını belirtmenize olanak tanır. Bu aşırı yükleme, öğe başına çalışmanın o kadar düşük olduğu ve öğe başına bir sanal yöntem çağrısının bile performans üzerinde belirgin bir etkisi olduğu senaryolarda kullanılabilir.
Özel Bölümleyiciler
Bazı senaryolarda, kendi bölümleyicinizi uygulamak faydalı olabilir, hatta gerekli olabilir. Örneğin, sınıfın iç yapısı hakkında bilginize bağlı olarak, varsayılan bölümleyicilerden daha verimli bir şekilde bölümleyebileceğiniz özel bir koleksiyon sınıfınız olabilir. Alternatif olarak, kaynak koleksiyondaki farklı konumlardaki öğelerin işlenmesinin ne kadar süreceğine ilişkin bilginize bağlı olarak farklı boyutlarda aralık bölümleri oluşturmak isteyebilirsiniz.
Temel bir özel bölümleyici oluşturmak için, aşağıdaki tabloda açıklandığı gibi öğesinden System.Collections.Concurrent.Partitioner<TSource> bir sınıf türetin ve sanal yöntemleri geçersiz kılın.
Metot | Açıklama |
---|---|
GetPartitions | Bu yöntem ana iş parçacığı tarafından bir kez çağrılır ve bir IList(IEnumerator(TSource)) döndürür. Döngüdeki veya sorgudaki her çalışan iş parçacığı, ayrı bir bölüm üzerinden bir almak IEnumerator<T> için listeden çağrı GetEnumerator yapabilir. |
SupportsDynamicPartitions | uygularsanız GetDynamicPartitionsdöndür, true aksi takdirde. false |
GetDynamicPartitions | ise SupportsDynamicPartitionstrue , bu yöntem isteğe bağlı olarak yerine GetPartitionsçağrılabilir. |
Sonuçların sıralanabilir olması gerekiyorsa veya öğelere dizinli erişime ihtiyacınız varsa, aşağıdaki tabloda açıklandığı gibi kendi sanal yöntemlerini türetin System.Collections.Concurrent.OrderablePartitioner<TSource> ve geçersiz kılın.
Metot | Açıklama |
---|---|
GetPartitions | Bu yöntem ana iş parçacığı tarafından bir kez çağrılır ve döndürür IList(IEnumerator(TSource)) . Döngüdeki veya sorgudaki her çalışan iş parçacığı, ayrı bir bölüm üzerinden bir almak IEnumerator<T> için listeden çağrı GetEnumerator yapabilir. |
SupportsDynamicPartitions | Uygularsanız GetDynamicPartitionsdöndür true ; aksi takdirde false. |
GetDynamicPartitions | Genellikle, bu yalnızca öğesini çağırır GetOrderableDynamicPartitions. |
GetOrderableDynamicPartitions | ise SupportsDynamicPartitionstrue , bu yöntem isteğe bağlı olarak yerine GetPartitionsçağrılabilir. |
Aşağıdaki tabloda, üç tür yük dengeleme bölümleyicisinin sınıfı nasıl uyguladığı OrderablePartitioner<TSource> hakkında ek ayrıntılar sağlanır.
Yöntem/Özellik | Yük Dengeleme olmadan IList / Dizi | Yük Dengeleme ile IList / Dizi | Ienumerable |
---|---|---|---|
GetOrderablePartitions | Aralık bölümleme kullanır | Belirtilen partitionCount için Listeler için iyileştirilmiş öbek bölümleme kullanır | Statik sayıda bölüm oluşturarak öbek bölümleme kullanır. |
OrderablePartitioner<TSource>.GetOrderableDynamicPartitions | Desteklenmeyen özel durum oluşturur | Listeler ve dinamik bölümler için iyileştirilmiş öbek bölümleme kullanır | Dinamik sayıda bölüm oluşturarak öbek bölümleme kullanır. |
KeysOrderedInEachPartition | Döndürür true |
Döndürür true |
Döndürür true |
KeysOrderedAcrossPartitions | Döndürür true |
Döndürür false |
Döndürür false |
KeysNormalized | Döndürür true |
Döndürür true |
Döndürür true |
SupportsDynamicPartitions | Döndürür false |
Döndürür true |
Döndürür true |
Dinamik Bölümler
Bölümleyicinin bir ForEach yöntemde kullanılmasını düşünüyorsanız, dinamik sayıda bölüm döndürebilmeniz gerekir. Bu, bölümleyicinin döngü yürütme sırasında herhangi bir zamanda isteğe bağlı yeni bir bölüm için bir numaralandırıcı sağlayabildiği anlamına gelir. Temel olarak, döngü yeni bir paralel görev ekledikten sonra bu görev için yeni bir bölüm talep eder. Verilerin sıralanabilir olmasını istiyorsanız, her bölümdeki her öğeye benzersiz bir dizin atanması için öğesinden System.Collections.Concurrent.OrderablePartitioner<TSource> türetin.
Daha fazla bilgi ve örnek için bkz . Nasıl yapılır: Dinamik Bölümleri Uygulama.
Bölümleyiciler için Sözleşme
Özel bölümleyici uygularken PLINQ ve ForEach TPL ile doğru etkileşimi sağlamaya yardımcı olması için şu yönergeleri izleyin:
için
partitionsCount
sıfır veya daha az bağımsız değişkeniyle çağrılırsaGetPartitions, at .ArgumentOutOfRangeException PLINQ ve TPL hiçbir zaman 0'a eşit birpartitionCount
değer geçirmese de, yine de bu olasılığa karşı korumanızı öneririz.GetPartitions ve GetOrderablePartitions her zaman bölüm sayısını döndürmelidir
partitionsCount
. Bölümleyicinin verileri tükenirse ve istenen sayıda bölüm oluşturamıyorsa, yöntemin kalan bölümlerin her biri için boş bir numaralandırıcı döndürmesi gerekir. Aksi takdirde, hem PLINQ hem de TPL bir InvalidOperationExceptionoluşturur.GetPartitions, GetOrderablePartitions, GetDynamicPartitionsve GetOrderableDynamicPartitions hiçbir zaman (
Nothing
Visual Basic'te) döndürülmemelidirnull
. Bunu yaparlarsa, PLINQ / TPL bir InvalidOperationExceptionoluşturur.Bölümleri döndüren yöntemler her zaman veri kaynağını tam ve benzersiz olarak numaralandırabilen bölümler döndürmelidir. Bölümleyicinin tasarımında özellikle gerekli olmadığı sürece veri kaynağında veya atlanan öğelerde yineleme olmamalıdır. Bu kurala uyulmazsa çıkış sırası karışık olabilir.
Çıkış sırasının karıştırılmaması için aşağıdaki Boole alıcılarının her zaman doğru şekilde aşağıdaki değerleri döndürmesi gerekir:
KeysOrderedInEachPartition
: Her bölüm, artan anahtar dizinleri olan öğeleri döndürür.KeysOrderedAcrossPartitions
: Döndürülen tüm bölümler için, i bölümündeki anahtar dizinleri i-1 bölümündeki anahtar dizinlerinden daha yüksektir.KeysNormalized
: Tüm ana endeksler sıfırdan başlayarak boşluklar olmadan monoton olarak artmaktadır.
Tüm dizinler benzersiz olmalıdır. Yinelenen dizinler olmayabilir. Bu kurala uyulmazsa çıkış sırası karışık olabilir.
Tüm dizinler nonnegative olmalıdır. Bu kurala uyulmazsa PLINQ/TPL özel durumlar oluşturabilir.