C# ile Paralel Programlama-6 (Koleksiyonlar İçerisinde Paralel İterasyonlar)

İlker Erhalim
3 min readJan 12, 2021
cottonbro adlı kişinin Pexels’daki fotoğrafı

Paralel programlamada koleksiyonlarla çalışırken, ihtiyacımıza göre Task oluşturabilir ve bu tasklerin özelliklerini kullanabiliriz. Ancak koleksiyonların boyutları büyüdükçe, iterasyon içerisinde yapılan işlemler arttıkça bu taskleri yönetmek zorlaşacak ve optimizasyon ihtiyaçları artacaktır, bu yüzden .net framework içerisinde koleksiyonlarla çalışmak için optimize edilmiş sınıf, extension ve metotları kullanmak daha sağlıklı olacaktır.

Paralel Döngüler

Paralel döngüler System.Threading.Tasks namespace altındaki Parallel sınıfı ile oluşturulabilinir. Paralel döngülerde döngü içeriği asenkron olarak çalışır, yani bir sonraki iterasyonun çalışması için bir öncekinin tamamlanması beklenmez.

Parallel.For

Paralel bir for döngüsü oluşturmak için kullanılır;

Parallel.For(0, 20, (i) =>
{
Console.WriteLine(i);
});

Parallel.ForEach

Paralel bir foreach döngüsü oluşturmak için kullanılır;

List<string> items = new List<string>() { “item-1”, “item-2”, “item-3”, “item-4”, “item-5”, “item-6”, “item-7”, “item-8”, “item-9”, “item-10” };
Parallel.ForEach(items, (item) =>
{
Console.WriteLine(item);
});

Aynı işlemi her bir iterasyon için bir task oluşturup yapabiliyorken neden Paralel bir döngü oluşturmalıyız?

Bir döngünün içerisinde Task oluşturduğunuzda her bir iterasyon için bir Task oluşturursunuz ve Task oluşturmak maliyetli bir işlemdir. Parallel sınıfındaki döngüler bunu önlemek için optimize edilmişlerdir.

Ayrıca Parallel sınıfı ile oluşturulan döngüler ile döngüye müdahale etme, çalışma şeklini belirleme ve döngü sonucu ile ilgili bilgiler almak oldukça kolaydır.

Bir döngüyü paralel çalıştırmak hızlı sonuçlanmasını sağlamayabilir.

Task oluşturmanın işlemci üzerinde maliyeti vardır ve çok kısa süren işlemler için bir task oluşturmak genelde daha yavaş çalışan uygulamalara sebep olur.

Yukarıdaki örneği çalıştırdığınızda senkron olan döngünün daha hızlı tamamlandığını göreceksiniz.

ParallelOptions

Paralel bir döngü oluştururken döngünün en fazla kaç Task oluşturacağı, hangi TaskScheduler üzerinde çalışacağı ve CancelationToken tanımını yapılabilinir.

ParallelLoopState

İterasyon içerisinde döngünün durumu hakkında bilgi almak ve döngüye müdahale etmek amaçlı kullanılır.

Normal bir döngü içerisinde continue ve break keywordlerini kullanabiliyorken paralel döngüler içerisinde bu keywordler kullanılamaz.

Döngü içerisinde parametre olarak gönderdiğimiz action aslında bir metot olduğu için continue yerine return kullanabiliriz.

Ancak break alternatifi biraz daha karışık;

Paralel bir döngü kullanırken iterasyon kısmı aynı anda çalıştığı için bu döngüyü aniden bitirmek mümkün değildir, ancak tanımladığımız actiona parametre olarak gönderilen ParallelLoopState örneğinden ihtiyacımıza göre Stop veya Break metodlarını kullanarak döngüyü sonlandırabiliriz.

  • stop: Döngüyü en kısa sürede sonlandırır.
  • break: Kendinden sonra çalışacak olan iterasyonları en kısa süre içerisinde sonlandırır.
Örnek kodun çıktısı, Break ile sonlandırılan döngüde 16'dan önceki tüm iterasyonlar çalışmış.

Resimde de görüldüğü gibi Break ile sonlandırılan döngüde 16' dan önceki tüm iterasyonlar çalışırken, stop ile sonlandırılan döngü en kısa sürede sonlandı.

Parallel Linq

.net ortamında koleksiyonlar ile çalışmak için genelde Linq tercih edilir, Linq extensionlarını aynı şekilde paralel olarak çalıştırmak mümkündür.

Bir koleksiyon üzerinde paralel linq çalıştırmak için System.Linq namespace altındaki AsParallel extension kullanılır. Bu extension parametre olarak aldığı koleksiyonu ParallelQuery<T> olarak geriye döndürür ve ParallelQuery<T> üzerine yazılmış extensionlar ile paralel linq çalıştırılabilinir.

int[] numbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
ParallelQuery<string> items = numbers.AsParallel().Select(number => $”Item-{number}”);
foreach (var item in items)
{
Console.WriteLine(item);
}
Console.ReadKey();

Yukarıdaki örneği çalıştırdığınızda her bir item için Item-x çıktısını aldığınızı göreceksiniz, ancak işlem paralel olarak yapıldığı için sıralama farklı olacaktır.

Paralel Linq işlemlerinde sıralı sonuç almak için AsOrdered extensionı kullanılabilinir.

int[] numbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
ParallelQuery<string> items = numbers
.AsParallel()
.AsOrdered()
.Select(number => $”Item-{number}”);
foreach (var item in items)
{
Console.WriteLine(item);
}
Console.ReadKey();

Bir önceki işlemin aynısını AsOrdered extensionı ile yapıldığında işlem yine paralel olarak yapılır, ancak sonuçlar sıralı olur.

ParallelQuery extensionları da normal Linq extensionları gibi GetEnumarator methodu çalışmadan Action/Func ları çalıştırmazlar.

Yukarıdaki örneği çalıştırdığınzda aldığınız çıktıda önce Before GetEnumarator olacaktır.

Before GetEnumarator.
Sırası öngörülemeyecek şekilde on rakam için Current Number:x çıktısı.
After GetEnumarator.

--

--