Senkron & Asenkron ve MultiThread Programlama -5 — Thread’ler Arası Haberleşme: Signalling

Alperen Öz
7 min readJun 14, 2024

--

Thread’ler Arası Haberleşme: Signalling

Photo by Campaign Creators on Unsplash

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:
  1. 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 parametresi false 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 direkt autoResetEvent.Set() diyerek sinyal gönderme işlemini gerçekleştirebiliriz.
  • Davranış:
  • thread1 başlatılır ve "Thread1" mesajını yazdırdıktan sonra autoResetEvent.Set() metodu ile sinyal verir.
  • thread2 başlatılır ve autoResetEvent.WaitOne() metoduyla thread1'den gelecek sinyali bekler. Sinyali aldıktan sonra "Thread2" mesajını yazdırır ve autoResetEvent.Set() metodu ile bir sonraki sinyal verir.
  • thread3 başlatılır ve autoResetEvent.WaitOne() metoduyla thread2'den gelecek sinyali bekler. Sinyali aldıktan sonra "Thread3" mesajını yazdırır ve autoResetEvent.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 parametresine false değeri verilirse, autoResetEvent nesnesi, sinyal bekleyen bir durumda görevine başlar ve direkt sinyal verilebilir. Eğer true değeri verilirse ilgili nesne sinyal verilmiş bir durumda görevine başlayacağı, yani autoResetEvent.Reset() diyerek manuel bir şekilde sinyal bekleyen yani çalışmaya hazır duruma geçirilmesi gerektiği anlamına gelir.

**initialState** parametresinin true 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 isteniyorsa Reset(); 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ından manualResetEventSlim.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 fakat manualResetEventSlim.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(); ile manualResetEventSlim.Reset(); arasına Thread.Sleap() eklenerek, kodun manualResetEventSlim.Reset(); metoduna erişmesi bekletilmiş ve diğer bekleyen thread1 ve thread2’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, ve thread3’ün her birinden sinyal geldikten sonra countdownEvent.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üne Thread.Sleep ile bekletme eklenmiştir.
  • thread1 ve thread2 ,Thread.Sleep ardından tamamlandı sinyalini gönderse bile thread4 aktif hale gelmek için thread3’ün de tamamlandı sinyalini beklemek zorundadır. Yani thread4 5500 ms sonra aktif olacaktır.

Main thread de countdownEvent.Wait(); ile bekletilebilir.

Kaynak: Gençay Yıldız — Asenkron & Multithread Programlama

--

--