Task Parallel Library (TPL)
Race Condition — Parallel Loops
Asenkron metotların en önemli özelliği, asenkron metodun çağrıldığı thread’in bloklanmaması ve birden fazla thread kullanıp/kullanmadığıyla ilgilenmemesiydi. Gerçi geçmiş zaman ifadesi kullansam da hala öyle. İlgilenmiyor yani. Şüpheye mi düştün? Asenkron & Multithread Programlama bir göz atabilirsin. Bununla ilgili klişe örnek olan restoran örneğini çokça duyarız. Örnekte, garson müşteriden aldığı siparişi şef’e 👩🏽🍳 ilettikten sonra şef’in ilgili siparişin hazırlamasını beklemek yerine farklı işlerle uğraşmasıyla ilgili bir durum söz konusuydu.
Task Parallel Library, Microsoft tarafından geliştirilen teknolojidir. TPL ile multithread programlamanın zahmetinden kurtuluyoruz.
Hatırlatmayı yaptıktan sonra restoran örneğine bir de farklı pencereden bakalım. 10 adet garsonumuz 💁♂️ (thread) olduğunu düşünelim. Bahsi geçen 10 garson, 200 adet müşterinin siparişlerini alıyor ve yemekleri de soğumadan ilgili müşteriye teslim edebiliyor. Buraya kadar her şey fevkalade. Fakat 1500 müşteri geldiğinde garsonlar (thread) nefes almadan çalışsalar 🏃🏼♂️ dahi siparişlerin teslimatında sorun yaşanacaktır ya da yemekleri soğuk şekilde servis edeceklerdir. Durumu birisi patrona 🤳🏾 haber verebilir mi? Garson sayısı yetersiz daha fazla garsona ihtiyacımız var. Alo!!!
Yukarıdaki senaryoyu yazılım ekosisteminde değerlendirirsek yoğun işlemleri birden fazla thread ile çalıştırıp kısa sürede sonuç almak isteyebiliriz. Bu isteğimizde bizlere TPL dediğimiz Task Parallel Library’nin kapılarını🚪aralar. Evet Hoş geldin! Dışarda kalma, gir içeri.. gel…
TPL ile zahmete girmeden yoğun işlemleri birden fazla thread üzerinde çalıştırabiliriz. Nasıl mı? Örneğin 5000–6000 öğeye (item) sahip listeye (List<object>) sahip olduğumuzu düşünelim. Eleman sayısı fazla olduğu için her bir öğenin işlenmesi, cevap dönmesi derken bu işlemi tek bir thread üzerinde çalıştırmaya yeltendiğimizde çok uzun süreceği kaçınılmaz gerçektir. Bu ve benzeri senaryolarda TPL tarafından sağlanmış For, Foreach hatta .Net 6 ile birlikte hayatımıza giren ForeachAsync gibi metotlar bulunmaktadır. Sağlanan bu metotlar ile TPL kütüphanesi 5000–6000 öğesi bulunan listeyi çeşitli parçalara bölüp, her bir parçaya da bir thread atayarak işlemleri gerçekleştiriyor. Dolayısıyla 5000–6000 öğeye sahip listeyi işlemek 10 dakika⌛sürüyorsa, birden fazla thread ile 1–2 dakikaya kadar indirgeyebiliriz. (süreler varsayımdır.)
Özetle, uygun senaryoları, birden fazla thread ile işlenmesini talep ediyorsak TPL (Task Parallel Library) kütüphanesi imdadımıza yetişiyor. Çünkü kütüphane, thread yönetim konusu olan;
- Görev atama
- Yaşam Döngüsü
gibi işleri yönetiyor, bizim ilgilenmemizi beklemiyor.
Kulağa hoş geldiğinin farkındayım fakat bazı dikkat⚠️etmemiz gereken hususlar var. Buyurun devam edelim.
Race Condition 🏇🏽
Multithread uygulamalarda birden fazla thread’in paylaşılan hafıza alanına aynı anda erişmesiyle meydana gelen durumdur.
Normal Karşılanan Senaryo (Tek Thread) 😊
Integer 17 değeri, birden fazla thread tarafından kullanılacak (paylaşılan hafıza) bir alandır. Her bir thread integer değeri alıp 1 arttıracak ve değeri güncelleyecek. Tek thread uygulamada 17 değeri 18 olarak güncellenip kaydedilir. Daha sonra 18 alınır 19, 19 alınır 20 şeklinde süreç devam eder ve doğru sonucu gün sonunda elde ederiz.
Normal Karşılanmayan (Race Condition) Senaryo 😢
Yukarıdaki görseli dikkate alarak başlayalım. Görsele ait uygulama çalıştırıldığında Thread A ve Thread B ile ayağa kalkıyor. Thread A ve Thread B, 17 değerini alıyor. T1 anında Thread A değeri 18, T2 anında da Thread B değeri 18 olarak güncelliyor. Daha sonra T3 anında Thread A değeri 18 olarak yazıyor. Daha sonra T4 anında da Thread B değeri 18 olarak yazıyor. Tabii ki böyle bir senaryo yaşamamamız gerekliydi. Normal de Thread A değeri 18 yaptıktan sonra Thread B’nin değeri 19 olarak yazmasını beklerdik fakat Race Condition durumu meydana geldi. Paylaşımlı bir alana 💾 birden fazla thread’in müdahalesi sonucu diğer threadlerin bayatlamış veriyle işlem yapmasından kaynaklandığı anlamış oldu.
Örneğin sayi 1 değerine (integer) sahibiz. int sayi=1;
Bu primitive type değerini 2 adet (X ve Y) thread çalıştırarak 1000 olarak güncellemeyi hedefleyelim. X thread’i değeri 500 kere arttırdı, son durumda 500 değerine ulaştık. Y threadi de 500 kere arttırdığını düşünürsek son durumda 1000 değerini elde etmeyi bekliyoruz. Fakat burada aynı veriyi aldıklarından (Race condition) dolayı genelde 1000 değeri değil de 900, 890, 850 gibi değerlerle karşılaşırız. Ancak ve ancak race condition durumu meydana gelmezse 1000 değerini de görebiliriz.
Race condition durumu her zaman yaşanacak diye düşünmemeliyiz. Ancak multithread programlama da kullanmış olduğumuz threadler paylaşımlı veriye erişmeye çalışıyorsa bu durum mutlaka göz 👀 önünde bulundurulmalıdır. Aksi takdirde gün sonunda istenilen davranış elde edilemeyecektir.
Sorunu Nasıl Çözerim?
Race condition durumunu engellemek adına paylaşılan hafıza kitlenir. 🔒 Örneğin konu başlığındaki resimde yer alan Integer 17 değeri kilitlenir. Thread A değeri alıp güncelleyene kadar başka bir thread’in bu değeri alması engellenir. Bu sayede Thread A değeri 18 yaptıktan sonra Thread B güncellemiş değeri alır ve artırım yapar, 19 olur. Bu durumun kötü yönü ise değeri kilitlediğimizden dolayı performans olumsuz yönde etkilenecektir. Multithread programlamada paylaşımlı veri (paylaşılan hafıza) kullanmıyorsak, herhangi bir ortak veriye erişmeye çalışmıyorsak geliştirilen kod daha hızlı çalışacaktır. Çünkü lock mekanizması olmayacağından dolayı her bir thread istediği şekilde işlemini gerçekleştirebilecektir.
Parallel.ForEach & Parallel.ForEachAsync
TPL kütüphanesinin sunmuş olduğu metotlardır. Alınan liste içerisindeki elemanları farklı threadler üzerinde çalıştırarak multithread kod yazmamıza imkan verir.
Çalışma Mekanizması: Örneğin 2000 adet öğeye sahip bir listeniz var. Bu liste öğelerini 250'lik parçalara ayırıyor ve her bir parçaya 1 thread görevlendiriyor. Her bir thread 🪡 kendi parçasından sorumlu oluyor. Bu sayede bizim ekstra thread yönetimiyle ilgili yapmamız gereken iş olmuyor. Foreach içerisine vereceğim listenin eleman sayısının fazla olmasının önemi vurgulanır. Çünkü 30–40 öğeye sahip listeyi vermek senkron koddan daha yavaş çalışabileceği bir durum yaratabilir. Microsoft dokümanlarında da uyardığı gibi bu gibi durumlarda mutlaka test yapılmalıdır. Çünkü bazı senaryolarda foreach döngülerinin veya multithread kodların daha yavaş çalışabileceğinden bahsediyor.
Örnek
Elimizde bulunan 55 adet rastgele fotoğrafı 60x60 boyutuna Foreach, Parallel.ForEach ve ParallelForEachAsync kullanarak hazırlayıp daha sonra kaydedelim. Haydi!
Paralel metotlarda dikkatinizi çekti mi? bilmiyorum ama bir thread birden fazla resmi küçük boyuta çevirmiş. Örneğin Parallel.Foreach metotunda 6, 4, 10 numaralı threadler. Bu sayı artabilir de azalabilirde. Ayrıca burada bir paylaşılan hafıza (shared data) paylaşmadığımız için performans kaybı yaşamadık. ✨
Parellel.ForEach’de Race Condition Durumundan Nasıl Sıyrılırım?
C#’da ref anahtar kelimesi değer tipleri referans tip olarak davranmasına olanak sağlıyor. System.Threading namespace altında yer alan Interlocked statik sınıfında (static class) yer alan statik metotlar üzerinden gideceğiz.
Race condition durumu her zaman oluşur diye yargı dağıtamayız.
Interlocked.Add(ref totoalFileByte, fileInfo.Length);
statik metotu herhangi bir thread totalFileByte
değerini güncellerken bir başka thread’in erişimini engellemektedir. 🚫 Bu senaryoda değeri kilitleme söz konusu olduğu için performans kaybına neden olacaktır.
- Increment: Paylaşılan hafızadaki değişkenin değerini thread safe olarak arttırır.
- Decrement: Azaltır
- Exchange: Değiştirir
Parallel.For/ForEach/ForEachAsync Thread-Local Variables
Paylaşımlı veri kısmında For/ForEach/ForEachAsync’i daha performanslı kullanabilmenin alternatifi bulunmaktadır.
Yukarıdaki görseli baz alarak, number üzerindeki arttırımın iş bölümlemesini şu şekilde düşünelim;
- Thread 1 → 1–25
- Thread 2 → 25–50
- Thread 3 → 50–75
- Thread 4 → 75–100
- Thread 5 → 100–125
Her thread kendi içerisinde toplamasını yaptıktan sonra en son paylaşılan veriyi güncelleyecek, bu işlemde bize performans sağlayacaktır. Neden? Çünkü, her thread paylaşılan veriye erişmek yerine kendi içerisinde değeri topladıktan sonra en son adım olan interlocked ile beraber paylaşılan vereyi güncelleyeceği için.
Tot ziens 😎