Paralel Programlama (PLINQ) ile Veri İşleme Gücünü Arttırma

Increasing Data Processing Power with Parallel Programming (PLINQ)

Cihat Solak
Intertech
8 min readFeb 26, 2023

--

Parallel Language Integrated Query (PLINQ)
Parallel Language Integrated Query (PLINQ)

LINQ, Net Framework 3.5 ile aramıza katılan ve diziler üzerinde sorgulama yeteneği ile okunabilirliği yüksek sorgular yazmamıza imkan veren teknolojidir.

LINQ teknolojisinin farklı varyasyonları bulunmaktadır. Örneğin

  • 🔹[LINQ to XML], XML üzerinde sorgular (query) çalıştırır.
  • 🔹[LINQ to Entity] entityler üzerinde sorgular çalıştırır.

PLINQ ise paralel olarak LINQ sorgularını çalıştırmamıza imkan verir. Net Framework 4.0 ile beraber

Formül: Paralel Programlama + LINQ = PLINQ

yukarıdaki formül ortaya çıkarak LINQ sorgularının paralel olarak çalıştırılmasına olanak sağladı.

Parallel Language Integrated Query (PLINQ)
https://linqsamples.com/tutorials/linq-for-beginners

Parallel Nedir?

Sorguların eş zamanlı olarak birden fazla thread (multithread) üzerinde çalıştırılmasıdır. Sorguların performansı, sorgunun çalıştırılmış olduğu sistemin işlemci ve çekirdek sayısına da (bir faktör) bağlıdır. 🚀 Buradan yola çıkarak işlemci ve çekirdek sayısı ne kadar yüksekse, sorguların da o kadar performanslı çalışacağını düşünebiliriz. Bu açıdan mükemmel bir kodlama ortaya çıkarsan da kodun çalışacağı makine de bir o kadar önemlidir.

LINQ | PLINQ

Yukarıdaki github gist üzerinden ilerleyelim. LINQ sorgusu, her bir aracın isminde Taycan içeriyor mu? diye kontrol eder. PLINQ sorgusunda ise AsParallel() metotu kullanıldığından dolayı, AsParalel() metotu sonrasında yazılacak her sorgu paralel şekilde çalıştırılacaktır.

- Paralel şekilde çalışmak/çalıştırmak ne demek?

Bir üstte yer alan vehicles listesinin 600 araç içerdiğini varsayalım. Sistemimizin 6 thread’e sahip olduğunu ve PLINQ araç listesi için 6 adet thread ayırdığını düşünelim. Bu parametrelerle yola çıkacak olursak, 600 araç 6 thread’e 100'erli şekilde paylaştırılacaktır. (Bu tür senaryolardaki rakamlar, konunun anlaşılır olabilmesi için varsayımdır. ⚠️)

Parallel Language Integrated Query (PLINQ)
PLINQ —Multithreaded Programming

Her thread kendisine düşen 100 öğenin her birinin isminde taycan geçiyor mu? diye kontrol edecek, sonuç olarak 6 thread’den gelen sonuçlar birleştirilip dönülecek. Performans dediğimiz vitamin 🍊aslında tam olarak burada!! Örneğin, bahsini geçirdiğimiz sorguyu tek çekirdekli işlemcide çalıştırmak 30 saniye sürüyorsa, 6 çekirdekli işlemcide çalıştırmak 5 saniye sürecektir (süreler sadece varsayımdır). Özetle çekirdek sayımız ve/veya işlemci sayımız ne kadar fazlaysa bir o kadar da performans artışı elde etmemiz olasıdır.

- Parallel.For & Parallel.Foreach’den farkı nedir?

Parallel Language Integrated Query (PLINQ)
Parallel.For | Parallel.Foreach

Yukarıdaki AsParallel() metoduyla oluşturulmuş sorgunun Parallel.For/Foreach’den işlem olarak farkı yoktur. Fark olarak okunabilirliği ele alabiliriz. AsParallel() metoduyla tek satırda halledebileceğimiz işi Parallel.For/Foreach ile birden fazla satırda halledebiliyoruz. Görüyoruz ki PLINQ teknolojisinin amacı Paralel’in gücü ile LINQ gücünü birleştirmek ve bu birleşim sonucunda daha okunabilir kodlar yazmaktır. Geldik mi yine senaryo 📹 konusuna? İçinde bulunduğun senaryoya göre Parallel.For veya Parallel.Foreach kullanmak tamamen senin tercihin!

1- AsParallel()

PLINQ sorguları yazabilmek için mutlaka AsParallel() metotunu kullanmalıyız. AsParallel() metotu sonrasında yazacağımız sorguların tamamı paralel şekilde birden fazla thread’de çalıştırılacak ve sonuç kümelerinin tamamı birleştirilip dönülecektir.

Parallel Language Integrated Query (PLINQ)

Console çıktısına baktığımızda 4, 8, 16, 20 … şeklinde, bir sıra içerisinde dizilim olmuş. Ancak sıralama her zaman böyle olacaktır! şeklinde bir kanıya varamayız. Çünkü AsParallel() metotuyla beraber 2500 item N tane thread’e bölündüğünü düşünürsek

Her thread [ 2500 / N = X ] — → X kadar item işleyecektir.

Tüm threadlerdan elde edilen sonuçlar birleştirilip foreach döngüsüyle beraber sonucu alırız. Çıktının dizilimi tek tek çalıştırıyormuşuz hissiyatı verse de burada paralel işlem gerçekleştirilmektedir.

Konuyu Toparlayalım 📚

LINQ vs PLINQ

4'üncü ve 10'uncu satırdaki kodlar aynı gibi görünse de 10'uncu satırdaki sorguya AsParallel() metodu dahil edilerek Paralel işleme tabii tutulmuştur.

Gist’de yer alan paralel işlem üzerine konuşalım. Burada Process metotunun her bir işlemi farklı sürelerde tamamladığını düşünelim. Bu durumda 2, 4, 6, 8, 10 gibi sıralı bir çıktı almayız. Çıktı olarak 22, 4, 16, 66, 78 gibi sırasız bir çıktı elde ederiz. Neden? Çünkü AsParallel() metoduyla yazdığımız sorgularda array listesi parçalara bölünerek işlenir ve işlemler bittikten sonra sırasız birleştirilir. Fakat parallel olmayan 4'uncu satırdaki LINQ sorgusunu ele alırsak, ilk işlem (Process metotu) 2 dakika sürse dahi bir sonraki sayıya geçmeyeceği için çıktı olarak sıralı bir çıktı elde ederdik.

Parallel sorgular array içerisindeki sırayı korumak zorunda değildir. ⚡

2- ForAll()

Parallel Language Integrated Query (PLINQ)
PLINQ | ForAll

PLINQ sorgusundan dönen sonucu (array) birden fazla thread’de işlemek için kullanılır. Çalışma mekanizması foreach gibidir fakat foreach (Parallel.Foreach’den bahsetmiyorum, normal foreach) tek thread’de elemanları tek tek işliyorken ForAll() birden fazla thread üzerinde bu işlemi gerçekleştirir. Bu metot, kaynaktaki her bir elementi paralel (multithread) şekilde çağırıyor.

ForAll() metodunu sadece ParallelQuery tipli array üzerinde kullanabilir, normal array’de üzerinde kullanılamaz. ⚡

cast
ForAll — Multithreading

Console çıktısına baktığımızda sayıların sıra düzeninin değiştiğini görebiliriz. Neden? Çünkü, ForAll() metotu Array içerisindeki verileri birden fazla thread (multithread) üzerinde çalıştırıyor.

ParallelQuery tipine sahip listeyi ToList() ile listeye çevirip Foreach ile döngü yaratmamız önerilen bir yöntem değildir. Çünkü bu işlemi tek thread üzerine yıkacağı için performansız 🚥 olacaktır. Eğer ToList()’e çevirip Foreach döngüsünü kullanırsak, bizi karşılayacak denklem aşağıdaki gibidir.

Formül: Liste Öğe Sayısı x İtem Başına Düşen İşlem Süresi = Toplam Süre

Yani ParallelQuery tipine sahip listeyi ToList() ile cast ettikten sonra Foreach ile döngü oluşturursak yukarıdaki denkleme denk gelecek süre kadar vakit geçirmiş ⏱️ oluruz. Ancak ForAll() metotu ile Array birden fazla thread’de çalıştırılacağından dolayı thread’ler birbirini bekleyecek olsa da multithread çalışma olduğu için dolayısıyla toplam sürede de azalma yaşayacağız.

Toparlamak amacıyla bir örnek daha görelim. ✍️

ForAll(number => {}) ile oluşturulan döngüde Thread.Sleep(500) yapılsa da işe koyulan birden fazla thread olduğu için (yukarıdaki örnekte genelde 4 thread çalıştı.) işe koyulan birden fazla thread veriyi işleyip uykuya geçecektir. Bu da toplam işlem süresinde azalma olacağı anlamına gelir. Örneğin 10 thread döngüye girdi veriyi ekrana yazdırdı uykuya geçti ve tekrar 10 thread veri işledi gibi.

9'uncu satırda ToList() edilip senkron’a çevrildiğinden ve tek thread çalıştığından dolayı verileri tek tek işleyip uykuya geçecektir. Bu da toplam işlem süresini uzatacaktır.

🖋️ Paralel olarak çalıştırılan her sorgu performanslı olacaktır, diyemeyiz. Senaryoya bağlı olarak paralel çalıştırdığımız sorgu senkrondan daha performansız çalışabilir ya da tam tersi durumla karşılaşılabilir. Yani senkron kod paralel kod’a göre daha performanslı olabilir. Senaryoyu oluşturduktan sonra mutlaka ölçmeli, test yazmalıyız.

🖋️ ️Olayın bütününe bakacak olursak; Eğer sahip olduğumuz Array az denecek öğe sayısına sahipse (50, 200, 300) bu durumda senkron olarak ele almak daha performanslı olabilir. Fakat öğe sayınız azken, her bir öğe için yapacağınız işlem yoğun bir işlemse (her bir item 15–25 saniye kadar sürecekse) paralel sorgu daha uygun olacaktır. Diğer yandan öğe sayısı fazlaysa (1000, 2500, 3800) bu durumda da paralel olarak çalıştırmak uygun olabilir. Test edilmeli! Daha sonra karar verilmelidir!

Entity PLINQ

appDbContext.Vehicles.AsParalel() sorgusu, veri tabanından araçları paralel olarak çektiğini düşünebiliriz ama öyle bir durum söz konusu değil. AsParallel() metotu, elde edilen sonuç üzerinde yani kendisinden sonra yazılacak sorgularda paralel işlem sağlar. Bu durumda veri tabanından araçların tamamı çekilecek, elde edilen tüm araçlar üzerinde paralel işlem sağlanacaktır.

3'üncı satırdaki sorgunun dönüş tipi List<Vehicle> değil, ParallelQuery<Vehicle> dir.

Şimdi github gist’i tekrar yorumlayalım. 3'üncü satırdaki sorgu veri tabanından fiyatı 10M’den büyük olanların 20 tanesini paralel şekilde sorgulamaz. 📢 Tüm araçları veri tabanından aldıktan sonra elde edilen liste üzerinde paralel sorgular gerçekleştirir. Akla bunun neresi performanslı? sorusu gelebilir fakat PLINQ amacı elde alan array üzerinde paralel sorgular gerçekleştirmektir.

İlk bakışta mantıksız geliyor olabilir.

  • 💫 Neden veri tabanına yazdığım sorguları yansıtmıyor?
  • 💫 Neden tüm listeyi alıyor?
  • 💫 Performans nerde?

gibi görünebilir fakat PLINQ amacı bu değil. Örneğin veri tabanından almış olduğumuz araçları bir dosyaya yazdırmak isteseydik Paralel işimizi kolaylaştıracaktı. (Kesinlikle, senaryoya göre.)

Yukarıdaki gist’i SQL Profiler ile izlemekte fayda var. ParalelQuery’i IQueryable mantığında düşünebiliriz. Sorguları tamamladıktan sonra ForAll() metotuna kadar veri tabanına ilgili sorguyu yansıtılmayacaktır. Anlaşılan biz ToList() ya da ForAll() metotunu kullandığımız satıra kadar veri tabanına sorgu yansıtılmaz.

3- WithDegreeOfParalleism()

Sorguların isteğe bağlı olarak, kaç işlemcide çalışacağını belirtebileceğimiz metottur. WithDegreeOfParalleism(3) olarak belirlersek ve sorgunun işleneceği makine de 4 işlemci varsa sadece 3 işlemci kullanılacaktır.

4- WithExecuteMode()

Yazdığımız her paralel sorgu kesin olarak paralel şekilde çalışacaktır diyemeyiz. 😯 Haydaaa! deme iyiliğimiz için… 😯

PLINQ yazdığımız sorguyu kendi içerisinde değerlendiriyor. Eğer sorgunun senkron olarak çalışmasını uygun görürse senkron, paralel çalışmasını uygun görürse paralel olarak çalıştırıyor. Ama sen diyorsan ki, benim yazdığımız sorgu kesinlikle paralel çalışmalı! O zaman WithExecuteMode() metotunu kullanacağız. Bu metot parametre olarak enum almaktadır.

Sorgunun paralel olarak çalıştırılıp çalıştırılmayacağının kararını PLINQ’ya bırakmak makbul olanıdır. Çünkü PLINQ arka tarafta sahip olduğu algoritmaları kullanarak yazılan sorgunun en kısa sürede hangi yoldan çalışacağını belirleyip, işleme alıyor.

5- AsOrdered()

Parelel programlamada birden fazla thread devreye girip işlem yapar ve işlem sonucunda ortaya çıkan sonuçlar rastgele birleştirilip dönülür. Yani, Girdi sıralaması ile çıktı sıralaması eşit olmayabilir.🥢Özellikle, girdi array’i içerisindeki öğelerin sıralaması önemli ve bu sıralamanın korunması gerekiyorsa, AsOrdered() metotu kullanılmalıdır. Ancak bu metot performansta düşüş sağlayacağı için dikkatli kullanılması gereklidir.

Hata Yakalama (Exception Handle)

Paralel sorgularda eş zamanlı olarak farklı thread’ler farklı array parçalarını işlediğinden dolayı her bir thread’den aynı anda birden fazla exception fırlayabilir. Çok uzun bir cümle oldu. Multithread olduğu için, örneğin 5 thread çalışıyor. Bunların 2 sinde hata olduğunda geriye 2 adet exception dönecektir. Sanırım şimdi oldu. Biz bu hataları tek seferde AggregateException yardımıyla yakalayabiliriz.

AggregateException içerisinde birden fazla exception barındırabilir. Yukarıdaki kod satırında 7 exception fırlatılsa da AggregateException ile 15'inci satırda hepsini okuyup handle edebilirim.

Yalnız 3'üncü satırdaki where(IsControl) koşuluna try-catch eklemeseydim ve o kısımda hata alsaydı program devam etmeyecekti. Where Func<Vehicle, bool> tiplidir. Program akışının hata alsa da devam etmesi adına IsControl static metotu oluşturup try-catch blokları arasında kontrol sağladım. Bu kısım senaryonuza göre değişiklik gösterebilir.

Ayrıca AggregateException içerisinde belirli tipteki hataları ayıklamak isterseniz 17'inci satırdaki gibi is ifadesini kullanabilirsiniz.

À bientôt 👋

--

--