Threadlerin Doğru Yönetilmesi Ve Race Condition
Selamlar,
Bugün bir projede karşılaştığım race condition durumunu, sizler ile bu makalede tartışmak istiyorum. Aynı anda çalışan farklı Taskların birbirlerine bağımlı yani, çalışma zamanlarının birbirleri ile kesişmeden yürütülmesi gerekiyordu. Biz bu soruna Race Condition diyoruz.
Paylaşılan memoryi kullanan processlerin, kendisine ait işleri yanlış zamanda yapıp, verilerin bozulduğu duruma race condition denir. Esas amaç processlerin doğru sırada çalışmasının sağlanmasıdır. Mesela hangi process’in önce çalıştırılacağı, oradaki değeri hangisinin alacağı ve nasıl değiştireceği race condition’ı ilgilendiren en güzel örneklerdir. Race, kelimesinden de anlaşılacağı gibi, processlerin birbirleri ile yarışmasıdır. Hangi processin hangi zamanda çalıştıracağı tamamen bizim sorumluluğumuzdadır.
Öncelikle gelin problem çıkabilecek senaryomuzu oluşturalım: 3 farklı methodumuz var.
- Ekrana 5 tane “*” karakteri basacak.
- Ekrana 5 tane “+” karakteri basacak.
- Ekrana 5 tane “$” karakteri basacak.
Aşağıdaki kodun ekran çıktısı sürekli farklılık gösterir. Örnek 4 ekran çıktısı aşağıdaki gibidir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; using System.Web.Mvc; namespace TaskRace.Controllers { public class HomeController : Controller { public int Count = 5; string result = "<b>Password : </b>"; static object locker = new object(); // GET: Home public ActionResult Index() { Task t1 = Task.Factory.StartNew(SendStar); Task t2 = Task.Factory.StartNew(SendPlus); Task t3 = Task.Factory.StartNew(SendDolar); Task[] taskList = new Task[] { t1, t2,t3 }; Task.WaitAll(taskList); return Content(result); } public void SendStar() { for (int i = 0; i < Count; i++) { Task.Delay(1000); result += "*"; } } public void SendPlus() { for (int i2 = 0; i2 < Count; i2++) { Task.Delay(1000); result += "+"; } } public void SendDolar() { for (int i3 = 0; i3 < Count; i3++) { Task.Delay(1000); result += "$"; } } } } |
Halbuki amacımız ekrana birbirlerine karışmadan 5 tane “*” , “+” ve “$” yazdırmaktır. Bu durumda tasklar birbirlerini beklemek zorundadırlar. İşte bugün bu sorunu çözebilecek 4 farklı yöntemi inceleyeceğiz.
Image Source: https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/Thread_pool.svg/580px-Thread_pool.svg.png
1-) Lock İle Synchronized Etmek: Aşağıda görüldüğü gibi ortak kullanılan static bir “lock” object ile ilgili taskler birbirlerini beklemektedir. Bu açıdan bakıldığında ilgili taskların parallel çalışılması engellenmiş ama bağımlı işlemlerin birbirini ezmesi durumunun önüne geçilmiştir.
Örnek Ekran Çıktısı:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; using System.Web.Mvc; namespace TaskRace.Controllers { public class HomeController : Controller { public int Count = 5; string result = "<b>Password : </b>"; static object locker = new object(); // GET: Home public ActionResult Index() { Task t1 = Task.Factory.StartNew(SendStar); Task t2 = Task.Factory.StartNew(SendPlus); Task t3 = Task.Factory.StartNew(SendDolar); Task[] taskList = new Task[] { t1, t2, t3 }; Task.WaitAll(taskList); return Content(result); } public void SendStar() { lock (locker) { for (int i = 0; i < Count; i++) { Task.Delay(1000); result += "*"; } } } public void SendPlus() { lock (locker) { for (int i2 = 0; i2 < Count; i2++) { Task.Delay(1000); result += "+"; } } } public void SendDolar() { lock (locker) { for (int i3 = 0; i3 < Count; i3++) { Task.Delay(1000); result += "$"; } } } } } |
Aynı işlemleri “Task“‘ler yerine “Thread” ile yapmak istese idik aşağıdaki gibi bir çözüme gitmemiz gerekecekti:
Not: Yukarıdaki örnekde aklınıza takılan bir durum olabilir :) Hangi Task’in önce başlayacağı belirlenememektedir. Zaten Race durumu, adını da tam buradan almıştır. Hangi taskin başlıyacağı, her çalıştırma işleminde değişmektedir. İşte aşağıdaki “Join()” methodu ile istenen Thread’in istenen sırada başlatılması ve birbirini beklemesi sağlanmıştır.
2-) Thread.Join() İle Synchronized Etmek :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; using System.Web.Mvc; namespace TaskRace.Controllers { public class HomeController : Controller { public int Count = 5; string result = "<b>Password : </b>"; // GET: Home public ActionResult Index() { var T1 = new Thread(SendStar); T1.Start(); T1.Join(); var T2 = new Thread(SendPlus); T2.Start(); T2.Join(); var T3 = new Thread(SendDolar); T3.Start(); T3.Join(); return Content(result); } public void SendStar() { for (int i = 0; i < Count; i++) { Task.Delay(1000); result += "*"; } } public void SendPlus() { for (int i2 = 0; i2 < Count; i2++) { Task.Delay(1000); result += "+"; } } public void SendDolar() { for (int i3 = 0; i3 < Count; i3++) { Task.Delay(1000); result += "$"; } } } } |
3-) Task.ContinueWith İle Synchronized Etmek : Peki “Thread” değil de istenen bir “Task“‘den başlanılması ve senkron bir şekilde çalışması istense idi yapılması gerekenler ne idi? İşte bu noktada “ContinueWith()” methodu devreye girmektedir. Başlatılan thread’in bitmesi durumunda, method içine yazılan yeni thread çalıştırılır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; using System.Web.Mvc; namespace TaskRace.Controllers { public class HomeController : Controller { public int Count = 5; string result = "<b>Password : </b>"; // GET: Home public ActionResult Index() { Task T1 = Task.Factory.StartNew(SendStar); Task T2 = T1.ContinueWith(antacedent => SendPlus()); Task T3 = T2.ContinueWith(antacedent => SendDolar()); Task[] taskList = new Task[] { T1, T2, T3 }; Task.WaitAll(taskList); return Content(result); } public void SendStar() { for (int i = 0; i < Count; i++) { Task.Delay(1000); result += "*"; } } public void SendPlus() { for (int i2 = 0; i2 < Count; i2++) { Task.Delay(1000); result += "+"; } } public void SendDolar() { for (int i3 = 0; i3 < Count; i3++) { Task.Delay(1000); result += "$"; } } } } |
4-) Monitor Enter İle Synchronized Etmek : Son olarak aşağıdaki örnekde “Monitor.Enter() ve Monitor.Exit()” methodu ile thread’lerin birbirlerini beklemesi sağlanmıştır. “Lock statement“‘ın aynısıdır. Aşağıda farklı olan durum, tüm “Thread”‘lerin bir List’e eklenmesi ve daha sonra tümünün tamamlanmasının “Any()” linq methodu ile beklenilmesidir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; using System.Web.Mvc; namespace TaskRace.Controllers { public class HomeController : Controller { public int Count = 5; string result = "<b>Password : </b>"; static object locker = new object(); // GET: Home public ActionResult Index() { List<Thread> threads = new List<Thread>(); var T1 = new Thread(SendStar); threads.Add(T1); var T2 = new Thread(SendPlus); threads.Add(T2); var T3 = new Thread(SendDolar); threads.Add(T3); T1.Start(); T2.Start(); T3.Start(); while (threads.Any(th => th.ThreadState == ThreadState.Running)) { } ; return Content(result); } public void SendStar() { Monitor.Enter(locker); try { for (int i = 0; i < Count; i++) { result += "*"; } } finally { Monitor.Exit(locker); } } public void SendPlus() { Monitor.Enter(locker); try { for (int i2 = 0; i2 < Count; i2++) { result += "+"; } } finally { Monitor.Exit(locker); } } public void SendDolar() { Monitor.Enter(locker); try { for (int i3 = 0; i3 < Count; i3++) { result += "$"; } } finally { Monitor.Exit(locker); } } } } |
Böylece Multithreading bir projede, threadlerin istenen sırada ve birbirlerini ezmeden nasıl çalıştırılabileceklerini hep beraber inceledik.
Bir maklenin daha sonuna geldik.
Yeni bir makalede görüşmek üzere hoşçakalın.
Kaynaklar: https://www.csharpstar.com/csharp-race-conditions-in-threading/
Faydalı ve anlaşılır bir makale olmuş teşekkürler.
Teşekkürler Ali..
Çok güzel ve faydalı bir yazı.
Teşekkürler Can.
İşine yaradı ise ne mutlu bana :)
Teşekkürler Bora bey , ellerinize sağlık
Ben teşekkür ederim Cengiz.