Senkron & Asenkron ve MultiThread Programlama -5 — Thread’ler Arası Haberleşme: Signalling
Thread’ler Arası Haberleşme: Signalling
Signalling Nedir? Ne Amaçla Kullanılır?
- Signalling, multi-threading programlamada thread’ler arası iletişimi ve senkronizasyonu sağlamak için kullanılan yapılardır.
- Signalling, bir thread’in, bir başka thread’e belirli bir olayın gerçekleştiğini bildirmesi ve bu bildirim sonucunda yapılacak işlemin devam ettirilmesi amacıyla kullanılır.
- Bu olaylar, ilgili thread’in işleminin tamamlanması, bir koşulun karşılanması veya bir kaynağın serbest olması gibi durumlar olabilir.
- Signalling, multi-threading programlama sürecinde race condition durumlarına karşı daha optimize bir yaklaşım sağlar, critical section’a erişimi kontrol eder ve thread’ler arasında güvenli paylaşım sağlar.
- Bu durumların değişikliklerini, thread’lerin operasyon süreçlerinde birbirlerine haber verebilmeleri için signalling kullanılır.
Özetle, signalling, thread’ler arası iletişim kurmak ve koordinasyon sağlamak amacıyla kullanılan önemli bir tekniktir.
Signalling Yaklaşımında Kullanılan Araçlar
- AutoResetEvent: Bir thread belirli bir olayın gerçekleşmesini bekler ve o olay gerçekleştiğinde otomatik olarak işleme devam eder.
- ManualResetEventSlim: Birden fazla thread’in belirli bir olayın gerçekleşmesini beklemesini sağlar. Manuel olarak sıfırlanabilir bir olay sinyali sağlar.
- CountdownEvent: Belirli bir sayıda thread’in tamamlanmasını beklemek için kullanılır.
AutoResetEvent Nedir?
- AutoResetEvent, bir thread’in belirli bir olayın gerçekleşmesini beklemesini ve bu olay gerçekleştiğinde diğer thread’in işleme devam etmesini sağlayan bir signalling ve senkronizasyon aracıdır.
- Olay gerçekleştiğinde, AutoResetEvent sinyali, sinyal bekleyen birden fazla thread olsa bile tek bir bekleyen thread’e gönderir ve sinyal gönderildikten sonra otomatik olarak sıfırlanır. Bu, sadece tek bir thread’in devam etmesine izin verir. Bazı kritik özellikleri:
- Tek Thread Devam Eder:
- AutoResetEvent, sinyal gönderildiğinde sadece bir bekleyen thread’in işleme devam etmesini sağlar.
- Turnike Metaforu:
- AutoResetEvent, sadece bekleyenler arasından tek bir thread’in işleme devam etmesini sağlar ve bu durum turnikelere benzer. Turnikede, her seferinde sadece bir kişi geçebilir ve geçiş yaptıktan sonra diğer kişiye geçiş izni verilir.
- Turnikede biri geçtiğinde diğer turnike meşgul olur ve geçiş hakkı verilene kadar diğerleri bekler. AutoResetEvent de benzer şekilde çalışır; bir thread sinyal aldığında diğer thread’ler bekler ve işlem sırası korunur.
2. Otomatik Sıfırlama:
- Olay gerçekleştiğinde sinyal gönderildikten sonra otomatik olarak sıfırlanır. Bu, diğer thread’lerin sinyali tekrar beklemeye başlamasını sağlar.
3. Kuyruk Oluşturma:
- Birden fazla thread
WaitOne
metodunu çağırırsa, AutoResetEvent bir kuyruk oluşturur ve thread'leri sıraya alır. Sinyal geldiğinde sıradaki thread işleme devam eder.
int i = 0;
AutoResetEvent autoResetEvent = new(false); //initialstate parametresi => false
Thread thread1 = new(() =>
{
while (i<=10)
{
i++;
Console.WriteLine("Thread1:" +i);
}
autoResetEvent.Set(); //görev tamamlandı sinyali gönderiliyor...
});
Thread thread2 = new(() =>
{
autoResetEvent.WaitOne(); //çalışmak için sinyal bekler durumda bir thread...
while (i < 20)
{
i++;
Console.WriteLine("Thread2:" + i);
}
autoResetEvent.Set(); //görev tamamlandı sinyali gönderiliyor...
});
Thread thread3 = new(() =>
{
autoResetEvent.WaitOne(); //çalışmak için sinyal bekler durumda bir thread...
while (i < 30)
{
i++;
Console.WriteLine("Thread3:" + i);
}
autoResetEvent.Set();
});
thread1.Start();
thread2.Start();
thread3.Start();
//output
//Thread1:1
//Thread1:2
//Thread1:3
//Thread1:4
//Thread1:5
//Thread1:6
//Thread1:7
//Thread1:8
//Thread1:9
//Thread1:10
//Thread1:11
//Thread2:12
//Thread2:13
//Thread2:14
//Thread2:15
//Thread2:16
//Thread2:17
//Thread2:18
//Thread2:19
//Thread2:20
//Thread3:21
//Thread3:22
//Thread3:23
//Thread3:24
//Thread3:25
//Thread3:26
//Thread3:27
//Thread3:28
//Thread3:29
//Thread3:30
- AutoResetEvent nesnesi, bir threadden başka bir threade sinyal verebilecek yetiye getirilmelidir. Bunun için 2 yöntem vardır. InitialState parametresine verebileceğimiz true veya false değeri ile bu davranış farklılık gösterir.
initialState = false
initialState
parametresifalse
olarak ayarlandığında,AutoResetEvent
nesnesi başlangıçta sinyal bekleyen bir durumda olur.- Yani
false
değerini verdiğimizde artık bu nesne, signaling aracıyla sinyal verebilecek bir durumda olduğunu belirtmiş oluyoruz. AutoResetEvent
nesnesine başka bir işlem yapmadan direktautoResetEvent.Set()
diyerek sinyal gönderme işlemini gerçekleştirebiliriz.- Davranış:
thread1
başlatılır ve "Thread1" mesajını yazdırdıktan sonraautoResetEvent.Set()
metodu ile sinyal verir.thread2
başlatılır veautoResetEvent.WaitOne()
metoduylathread1
'den gelecek sinyali bekler. Sinyali aldıktan sonra "Thread2" mesajını yazdırır veautoResetEvent.Set()
metodu ile bir sonraki sinyal verir.thread3
başlatılır veautoResetEvent.WaitOne()
metoduylathread2
'den gelecek sinyali bekler. Sinyali aldıktan sonra "Thread3" mesajını yazdırır veautoResetEvent.Set()
metodu ile bir sonraki sinyal verir.
Burada thread1 başladıktan sonra AutoResetEvent doğası gereği, thread1’den gelen sinyal tek bir thread’e ulaşacak ve tek bir thread işlemine başlayacak. Bu thread1 sonrası sinyal alıp işlemine başlayan thread yukarıdaki çıktıda görüldüğü üzere, bir çalışmada thread2 olabilirken bir diğer çalışmada thread3 olabilir. Yani bu kodun çıktısı thread1-thread3-thread2 sıralamasında da olabilirdi.
AutoResetEvent autoResetEvent = new(true);
Thread thread1 = new(() =>
{
autoResetEvent.Reset(); //sinyal bekler duruma geçirilir...
Console.WriteLine("Thread1");
autoResetEvent.Set();
});
Thread thread2 = new(() =>
{
autoResetEvent.Reset();
autoResetEvent.WaitOne();
Console.WriteLine("Thread2");
autoResetEvent.Set();
});
Thread thread3 = new(() =>
{
autoResetEvent.Reset();
autoResetEvent.WaitOne();
Console.WriteLine("Thread3");
autoResetEvent.Set();
});
thread1.Start();
thread2.Start();
thread3.Start();
- autoResetEvent nesnesi true olarak başlatıldığı için, ilk başta sinyal verilmiş durumda başlar. Ancak, tüm thread’ler başlatıldığında ilk işlemleri autoResetEvent.Reset() metodu çağrısı ile sinyali sıfırlamalı, yani sinyal bekler duruma getirilmelidir.
Yani özetle
initialState
parametresinefalse
değeri verilirse,autoResetEvent
nesnesi, sinyal bekleyen bir durumda görevine başlar ve direkt sinyal verilebilir. Eğertrue
değeri verilirse ilgili nesne sinyal verilmiş bir durumda görevine başlayacağı, yaniautoResetEvent.Reset()
diyerek manuel bir şekilde sinyal bekleyen yani çalışmaya hazır duruma geçirilmesi gerektiği anlamına gelir.*
*initialState**
parametresinintrue
olduğu case sayısı oldukça düşüktür.
ManualResetEventSlim Nedir?
ManualResetEventSlim
, thread'ler arasında sinyalleşme ve senkronizasyon sağlamak için kullanılan bir sınıftır.AutoResetEvent
'ten farklı olarak, sinyal verildiğinde manuel olarak sıfırlanana kadar bekleyen tüm thread'ler çalışmaya devam eder. Bu sınıf,ManualResetEvent
sınıfının daha performanslı ve düşük kaynak tüketen bir versiyonudur.- Bir başka deyişle,
ManualResetEventSlim
sinyal verildiği takdirde bir veya daha fazla bekleyen threadin işlemini başlatacaktır. Yani sinyal gönderildikten sonra bekleyen tüm threadler uyanacak ve sinyal neticesinde operasyonu işler hale getirecektir. Haliyle uyanan threadlerin kontrolü sağlanmak isteniyorsaReset();
fonksiyonu kullanılacaktır.
Özellikler ve Farklılıklar
- Manuel Sıfırlama:
ManualResetEventSlim
sinyal verildiğinde manuel olarak sıfırlanana kadar bekleyen tüm thread'ler çalışmaya devam eder. - Performans:
ManualResetEventSlim
,ManualResetEvent
'e kıyasla daha düşük kaynak tüketimi ve daha yüksek performans sunar. - Bekleyen Thread’lerin Serbest Bırakılması:
Set()
metodu çağrıldığında, sinyal verilir ve bekleyen tüm thread'ler serbest bırakılır.Reset()
metodu çağrılana kadar sinyal durumu devam eder.
int i = 0;
ManualResetEventSlim manualResetEventSlim = new(false);
Thread thread1 = new(() =>
{
while (i++ < 10)
{
Console.WriteLine("Thread1:" +i);
}
manualResetEventSlim.Set(); //diğer threadlere sinyal gider...
manualResetEventSlim.Reset(); //sinyal kesilir...
});
Thread thread2 = new(() =>
{
manualResetEventSlim.Wait();
while (i++ < 20)
{
Console.WriteLine("Thread2:" + i);
}
});
Thread thread3 = new(() =>
{
manualResetEventSlim.Wait();
while (i++ < 30)
{
Console.WriteLine("Thread3:" + i);
}
});
thread1.Start();
thread2.Start();
thread3.Start();
//output
//Thread1:1
//Thread1:2
//Thread1:3
//Thread1:4
//Thread1:5
//Thread1:6
//Thread1:7
//Thread1:8
//Thread1:9
//Thread1:10
- Örnekde görüldüğü gibi,
manualResetEventSlim.Set();
diyerek,manualResetEventSlim.Wait();
diğer sinyal bekleyen threadlerin hepsi aktif hale gelir, ardındanmanualResetEventSlim.Reset();
hamlesiyle de bu sinyal kesilir.
Yukarıdaki örneğin çıktısında sadece thread1’deki döngünün sonucunun görülmesinin sebebi, thread1 döngü işlemini tamamladıktan sonra
manualResetEventSlim.Set();
ile diğer bekleyen threadler aktif hale getirilmişdir fakatmanualResetEventSlim.Reset();
fonksiyonu diğer thread’lerden hızlı davranarak görevlerini yerine getirmelerine fırsat vermemiştir.
int i = 0;
ManualResetEventSlim manualResetEventSlim = new(false);
Thread thread1 = new(() =>
{
while (i++ < 10)
{
Console.WriteLine("Thread1:" +i);
}
manualResetEventSlim.Set();
//reset'den önce diğer threadlerin çalışmasına fırsat verilir.
Thread.Sleep(100);
manualResetEventSlim.Reset();
});
Thread thread2 = new(() =>
{
manualResetEventSlim.Wait();
while (i++ < 20)
{
Console.WriteLine("Thread2:" + i);
}
});
Thread thread3 = new(() =>
{
manualResetEventSlim.Wait();
while (i++ < 30)
{
Console.WriteLine("Thread3:" + i);
}
});
thread1.Start();
thread2.Start();
thread3.Start();
//output
//Thread1:1
//Thread1:2
//Thread1:3
//Thread1:4
//Thread1:5
//Thread1:6
//Thread1:7
//Thread1:8
//Thread1:9
//Thread1:10
//Thread2:12
//Thread2:14
//Thread2:15
//Thread2:16
//Thread2:17
//Thread2:18
//Thread2:19
//Thread2:20
//Thread3:13
//Thread3:22
//Thread3:23
//Thread3:24
//Thread3:25
//Thread3:26
//Thread3:27
//Thread3:28
//Thread3:29
//Thread3:30
Yukarıdaki örnekte yorum satırında da açıklandığı gibi,
manualResetEventSlim.Set();
ilemanualResetEventSlim.Reset();
arasınaThread.Sleap()
eklenerek, kodunmanualResetEventSlim.Reset();
metoduna erişmesi bekletilmiş ve diğer bekleyenthread1
vethread2
’nin çalışmasına fırsat verilmiştir.ManualResetEventSlim’ de çalışma mantığı ile bir kapıya benzetilir. Bi thread üzerinde yapılan işlem neticesinde
Set
metodu ile bir olay meydana geldiğinde yani sinyal verildiğinde kapı açılırcasına bekletinen tüm threadler kapıdan içeriye girecek yani çalışacak,Reset
metodu ile de bu kapı kapatılacaktır.
EventWaitHandle
- ManualResetEventSlim ve AutoResetEvent sınıfarının davranışlarını EventWaitHandle sınıfı aracılığıyla tek instance üzerinden yürütebiliriz.
- EventWaitHandle sınıfından bir instance oluştururken vereceğimiz 2. parametre ile signalling nesnesinin ManualResetEventSlim veya AutoResetEvent davranışlarından hangisini sergilemesini istediğimizi belirtiriz.
EventWaitHandle eventWaitHandle = new(false, EventResetMode.AutoReset); //AutoResetEvent
EventWaitHandle eventWaitHandle = new(false, EventResetMode.ManualReset); //ManualResetEventSlim
Thread thread1 = new(() =>
{
Console.WriteLine("Thread1");
eventWaitHandle.Set();
});
Thread thread2 = new(() =>
{
eventWaitHandle.WaitOne();
Console.WriteLine("Thread2");
});
Thread thread3 = new(() =>
{
eventWaitHandle.WaitOne();
Console.WriteLine("Thread3");
});
thread1.Start();
thread2.Start();
thread3.Start();
//autoReset output
//Thread1
//Thread2
//ManuelReset output
//Thread1
//Thread2
//Thread3
- Output’da görüldüğü gibi eventWaitHandle nesnesinin 2. parametresi EventResetMode.AutoReset olduğunda AutoResetEvent davranışı gösterilmiş, Thread1 ardından sadece tek bir thread aktif hale getirilmiş ve Thread2 çalışmış; EventResetMode.ManualReset olduğunda ManualResetEventSlim davranışı gösterilmiş ve Thread1 sinyal gönderdikten sonra tüm sinyal bekleyen threadler çalışmaya başlamışlardır.
CountDownEvent
- CountDownEvent, belirli bir sayıdaki threadlerin işlemini bitirmesinin ardından sinyal verilmesi üzerine davranış sergileyen bir araçtır.
- Büyük bir dosyanın parçalar halinde işlenmesi ve tüm parçaların tamamlanmasının beklenmesi veya birden fazla ağ çağrısının tamamlanmasını beklemek gereken case’lerde kullanımı faydalıdır.
//3 thread'den görev tamamlandı sinyalı bekliyorum...
CountdownEvent countdownEvent = new(3);
Thread thread1 = new(() =>
{
Console.WriteLine("Thread1");
Thread.Sleep(1000);
countdownEvent.Signal(); //görev tamamlandı..=> 2.sinyal
});
Thread thread2 = new(() =>
{
Console.WriteLine("Thread2");
Thread.Sleep(5500);
countdownEvent.Signal(); ////görev tamamlandı..=> 3.sinyal
});
Thread thread3 = new(() =>
{
Console.WriteLine("Thread3");
Thread.Sleep(800);
countdownEvent.Signal(); //görev tamamlandı..=> 1.sinyal
});
Thread thread4 = new(() =>
{
//çalışmak için sinyal bekliyor... 3. sinyalden sonra aktif
countdownEvent.Wait();
Console.WriteLine("Thread4");
});
thread1.Start();
thread2.Start();
thread3.Start();
thread4.Start();
//output
//Thread1
//Thread2
//Thread3
//Thread4
- Örnekte Görüldüğü üzere
countDownEvent
nesnesinin initial değeri 3 thread olarak verilmiştir. thread1
,thread2
, vethread3
’ün her birinden sinyal geldikten sonracountdownEvent.Wait();
ile bekletilen thread4, 3 farklı sinyale eriştikten sonra aktif olacaktır.- Bu bekleme işlemini daha net görülebilmesi için thread’lerdeki
countdownEvent.Signal();
metotlarının önüneThread.Sleep
ile bekletme eklenmiştir. thread1
vethread2
,Thread.Sleep
ardından tamamlandı sinyalini gönderse bilethread4
aktif hale gelmek içinthread3
’ün de tamamlandı sinyalini beklemek zorundadır. Yanithread4
5500 ms sonra aktif olacaktır.
Main thread de
countdownEvent.Wait();
ile bekletilebilir.