Makale Özeti

Artık multithreading uygulamalarda bizlere daha fazla esneklik getiren nesne ve metodlarını inceleyebiliriz. Önceki örneklerde gördüğünüz üzere uygulamamızda sadece bizim çalışmamız gereken yerler için kilit alma ve serbest bırakma mekanızmalarını kullandık. Peki, eğer kilit almak istediğimiz nesne üzerinde zaten kilit mevcutsa ve bu kilidi beklemek istemiyorsak ne yapabiliriz. Bu durumda Monitor.TryEnter() metodunu kullanabiliriz.

Makale

C#.NET ve Threading (Senkronizasyon nesneleri)

Thread Senkronizasyonu (Monitor.TryEnter, Monitor.Pulse, Monitor.PulseAll, Monitor.Wait)

            Artık multithreading uygulamalarda bizlere daha fazla esneklik getiren nesne ve metodlarını inceleyebiliriz.

            Önceki örneklerde gördüğünüz üzere uygulamamızda sadece bizim çalışmamız gereken yerler için kilit alma ve serbest bırakma mekanızmalarını kullandık. Peki, eğer kilit almak istediğimiz nesne üzerinde zaten kilit mevcutsa  ve bu kilidi beklemek istemiyorsak ne yapabiliriz. Bu durumda Monitor.TryEnter() metodunu kullanabiliriz.

public static bool TryEnter (Object obj)

            Tanımda gördüğünüz üzere TryEnter sayesinde nesne üzerinde kilit kurmaya çalışırız fakat o nesne üzerinde zaten kilit mevcut ise TryEnter metodu geriye “false” değeri döndürür ve sizde buna bağlı olarak program akışınızı değiştirebilirsiniz.

Aynı zamanda Monitor nesnesi Pulse(), PulseAll() ve Wait() olmak üzere iki ayrı metot daha sağlar. Ne işe yarar bu metodlar.

            PulseAll() Metodu kilit alınmış nesne üzerindeki kilidi kaldırır ve o nesneyi bekleyen diğer threadleri uyandırır. Ayni şekilde Pulse() metodu ise o an o nesneyi bekleyen diğer threadler içerisindeki ilk sıradakine sinyal yollar. Wait() metodu ise threadin almis oldugu kilidi bırakmasını ve o kilidin durumu değişene kadar beklemesini söyler. Çok genel bir örnekle bu işi açıklığa kavuşturalım.

Üretici/Tüketici(Producer/Consumer) problemi birden fazla thread ya da sürecin(process) birlikte kullandıkları ortak kaynaklara ulaşımındaki senkronizasyonu tanımlamak ve örneklemek için kullanılan genel bir örnektir(tabi genel bir örnek denildiğine bakıp küçümsemeyin işletim sistemlerinin processler arası mesaj gönderim ve alımı gibi bel kemiği işlemlerinden tutun i/o yönetimine kadar bir çok alanda uygulamasını görebilirsiniz.). Kabaca üretici(Producer) bir listeye elemanlar koyarken aynı zamanda tüketiciye(consumer) listeye eleman koyduğuna ve onu alması gerektiğine dair bir sinyal gönderir. Örneği inceleyelim

using System;
using System.Collections;
using System.Threading;

namespace MonitorsWait
{
  class Program
  {
    static ProducerConsumer queue;

    static void Main()
    {
      queue = new ProducerConsumer();
      Thread thread = new Thread(new ThreadStart(ConsumerJob));
      thread.Start();
      Random rng = new Random(0);
      for (int i = 0; i < 10; i++)
      {
        Console.WriteLine("Kuyruğa atılan değer {0}",i);
        queue.Produce(i);
        Thread.Sleep(rng.Next(500));
      }
      thread.Join();
      Console.Write("Program bitti bir tuşa basınız.");
      Console.ReadKey();
    }

    static void ConsumerJob()
    {
      Random rng = new Random(1);
      for (int i = 0; i < 10; i++)
      {
        object o = queue.Consume();
        Console.WriteLine("\t\tKuyruktan alınan değer {0}",o);
        Thread.Sleep(rng.Next(1000));
      }
    }
  }

  public class ProducerConsumer
  {
    readonly object queueLock = new object();
    Queue queue = new Queue();

    public void Produce(object o)
    {
      //queueLock kilidini al
      lock (queueLock)
      {
        //Queue içerisine değeri koy
        queue.Enqueue(o);
        //O an queueLock kilidini bekleyen thread var ise
        //queueLock kilidini serbest birak ve bekleyen thread'i uyar
        //Bekleyen bir thread var ise kilidi alacak ve işini yapacaktır.
        Monitor.Pulse(queueLock);
      }
    }

    public object Consume()
    {
      lock (queueLock)
      {
        //eğer queue içerisine konulmuş değer yoksa konulana kadar bekle.
        while (queue.Count == 0)
        {
          //queuelock üzerindeki kilidi kaldır ve tekrar kilidi alabilene kadar  //bekle
          Monitor.Wait(queueLock);
        }
        return queue.Dequeue();
      }
    }
  }
}

 

 

Program çıktısını inceleyelim

Kuyruğa atılan değer 0
                Kuyruktan alınan değer 0
Kuyruğa atılan değer 1
                Kuyruktan alınan değer 1
Kuyruğa atılan değer 2
                Kuyruktan alınan değer 2
Kuyruğa atılan değer 3
                Kuyruktan alınan değer 3
Kuyruğa atılan değer 4
Kuyruğa atılan değer 5
Kuyruğa atılan değer 6
                Kuyruktan alınan değer 4
Kuyruğa atılan değer 7
Kuyruğa atılan değer 8
                Kuyruktan alınan değer 5
Kuyruğa atılan değer 9
                Kuyruktan alınan değer 6
                Kuyruktan alınan değer 7
                Kuyruktan alınan değer 8
                Kuyruktan alınan değer 9
Program bitti bir tuşa basınız.

            Her bir kuyruğa eleman koyma işleminden sonra üretici tarafından sinyal gönderilen tüketici harekete geçmiş ve kuyruktan elemanları çekmiştir.

Not : Zamanlama farklarının threadler içerisindeki Sleep() metoduna bağlı olduğunu fark etmişsinizdir.

Örneğimizi adım adım inceleyelim.

public void Produce(object o)
    {
      //queueLock kilidini al
      lock (queueLock)
      {
        //Queue içerisine değeri koy
        queue.Enqueue(o);
        //O an queueLock kilidini bekleyen thread var ise
        //queueLock kilidini serbest birak ve bekleyen thread'i uyar
        //Bekleyen bir thread var ise kilidi alacak ve işini yapacaktır.
        Monitor.Pulse(queueLock);
      }
    }

  1. “queueLock” nesnesini kilitle ya da kilit alana kadar bekle

  2. Kuyruğa elemanı koy

  3. Monitor.Pulse ile birlikte queueLock üzerindeki kilidi kaldır ve o nesneyi bekleyen threadlere sinyal gönder.

       


public object Consume()
    {
      lock (queueLock)
      {
        //eğer queue içerisine konulmuş değer yoksa konulana kadar bekle.
        while (queue.Count == 0)
        {
          //queueLock Objesi uzerinde kilit var ise kilit kaldirilana kadar  bekle
          Monitor.Wait(queueLock);
        }
        return queue.Dequeue();
      }
    }
  1. queueLock nesnesini kilitle yada kilit alana kadar bekle

  2. Eğer kuyruk boş ise queueLock nesnesi üzerindeki kilidi kaldır ve Pulse sinyali alana kadar o nesneyi bekle. Üretici nesnesi kuyruğa yeni bir eleman koyduğunda o an beklemekte olan threade Pulse sinyali yollayacak ve bekleyen thread kaldığı yerden devam edecektir.

  3. Eğer kuyruk hala boşsa beklemeye devam et. Değilse kuyruktaki elemanı al.

Interlocking

            Yukarıda verdiğimiz örneklerde gördüğünüz üzere threadler arasında paylaşılacak kaynaklar üzerinde  hep kilit mekanizmaları kullandık. Şimdi ise .net ile birlikte gelen ve daha başka bir çok platform üzerinde sunulan interlocking mekanizmasını inceleyelim. Interlocking mekanizmasını threadler arası ortak kullanılan değişkenler üzerinde yapılan senkronize atomik işlemlerdir diye tanımlarsak yanlış olmaz. Peki bu değişkenler üzerindeki kontroller için daha önce gördüğümüz nesne kilitleme mekanizması yerine neden interlocking? Nedeni performans.

Static bir sınıf olan Interlocked  sınıfı ile değişken üzerinde arttırma,azaltma, değer okuma, değer ekleme, okuma , iki değeri değiştirme ve karşılaştırmalı değiştirme işlemleri yapılabilir.

Şimdi bir önceki örneklere benzer örneğimizi interlocking mekanizması ile yapalım.

using System;
using System.Threading;

namespace InterLocking
{
  class Program
  {
    private static int _counter;
    private static readonly object _lock = new object();
    static void Main()
    {
      ThreadStart job = new ThreadStart(ThreadJob);
      Thread thread = new Thread(job);
      thread.Start();
      for (int i = 1; i <= 5; i++)
      {
        Thread.Sleep(1000);
        Interlocked.Increment(ref _counter);
        Console.WriteLine("Ana thread {0} counter değeri {1}",i,_counter);
      }
      thread.Join();
      Console.WriteLine();
      Console.ForegroundColor = ConsoleColor.Magenta;
      Console.WriteLine("Program sonu counter değeri {0}",_counter);
      Console.WriteLine("Program bitti bir tuşa basınız.");
      Console.ReadKey();
    }
    static void ThreadJob()
    {
      for (int i = 1; i <= 5; i++)
      {
        Thread.Sleep(500);
        Interlocked.Decrement(ref _counter);
        Console.WriteLine("\t\t İşçi thread {0} counter değeri {1}",i,_counter);
      }
    }
  }
}

 

                        Programın çıktısında görüleceği üzere yine önceki örneklerde olduğu gibi herhangi bir senkronizasyon problemi ortaya çıkmadan threadler ortak değişken üzerinde arttırma ve eksiltme işlemlerini yapabildiler.

                 Isçi thread 1 counter degeri -1
Ana thread 1 counter degeri 0
                 Isçi thread 2 counter degeri -1
                 Isçi thread 3 counter degeri -2
Ana thread 2 counter degeri -1
                 Isçi thread 4 counter degeri -2
                 Isçi thread 5 counter degeri -3
Ana thread 3 counter degeri -2
Ana thread 4 counter degeri -1
Ana thread 5 counter degeri 0

Program sonu counter degeri 0
Program bitti bir tusa basiniz.

           

Süreçler Arası Senkronizasyon Nesneleri

            Multithreading programlamada, süreçler(process) arası iletişim ve kontrolün sağlanması için kullanılan nesnelere “Senkronizasyon” nesneleri denilir. Konuyu kavramak için bu nesneleri örnekleri ile birlikte inceleyelim.

Event’ler

            Herşeyden önce burada kastedilen Event mekanızması program geliştirme araçlarında kullandığımız olaylardan(event) farklıdır. Örneğin C# içerisinde bir buton tıklandığında meydana gelen Click() eventi gibi. Bu iki kavramı karıştırmamamız gerekiyor.

            Nedir bu Event? Event aslında aynı anda SET (Signalled) ya da RESET (Non-Signalled) durumundan biri olabilen, işletim sistemi düzeyinde global geçerliliğe sahip uygulamalar arası process senkronizasyon nesnesidir.

Wait Handles

            Adında anlaşılacağı üzere “WaitHandle” nesnesi “win32” platformu üzerinde işletim sistemi tarafından sağlanan event senkronizasyon nesnesini kullanır. WaitHandle sınıfı .net içerisinde abstract bir sınıf olarak uygulanmıştır. Kodlama sırasında bu sınıfdan türetilmiş senkronizasyon nesneleri kullanılacaktır. .net framework içerisinde WaitHandle sınıfından türetilmiş senkronizasyon nesnelerini sayarsak EventWaitHandle, AutoResetEvent, ManualResetEvent,Mutex ve Semaphore

 

EventWaitHandle

            Herşeyden önce iki kavram üzerine bir şeyler söylememiz gerekiyor. AutoResetEvent ve ManualResetEvent. .Net framework içerisinde EventWaitHandle yada bu sınıftan türetilmiş nesneleri kullanırken bu iki tanım karşımıza çıkıyor. Kabaca özetlememiz gerekirse AutoResetEvent kavramı bir event’i SET ettiğimizde event nesnesinin kendisini bekleyen bir süreç çalışır çalışmaz tekrar RESET moduna döndüğünü tanımlar. ManualResetEvent tanımı ise o event nesnesi SET edildiğinde tekrar RESET moduna dönebilmesinin programcının direktifine bağlı olduğunu belirtir. EventWaitHandle nesnesine parametre olarak geçilen EventResetMode enumeration’i ile eventimizin tipini belirleyebilir ya da EventWaitHandle sınıfından türetilmiş AutoResetEvent veya ManualResetEvent nesnelerini kullanabiliriz.

Bu iki ayrı kavramın kullanımı program geliştirirken daha iyi anlaşılacaktır.

            EventWaitHandle nesnesini daha iyi kavramak için aşağıdaki örneği inceleyelim.

using System;
using System.Threading;

namespace Events_WaitHandle
{
  public class Example
  {

      private const int _threadCount = 5;
      private static EventWaitHandle ewHandle;
      public static void Main()
      {
          ewHandle = new EventWaitHandle(false, EventResetMode.AutoReset);
          for (int i = 1; i <= _threadCount; i++)
          {
              Thread t = new Thread(
                  new ParameterizedThreadStart(ThreadJob));
              t.Start(i);
          }
          Thread.Sleep(500);
          for (int i = 1; i <= _threadCount; i++)
          {
            Console.WriteLine("Enter'a bastığınızda threadler çalışmaya başlayacak.");
            Console.ReadLine();
            ewHandle.Set();
          }
          Console.WriteLine();
          Console.WriteLine("Çıkmak için ENTER.");         
          Console.ReadLine();
      }

      public static void ThreadJob(object threadIndex)
      {
          int index = (int) threadIndex;
          Console.WriteLine("Thread {0} bekliyor.", index);
          ewHandle.WaitOne();
          Console.WriteLine("Thread {0} çalıştı ve çıkıyor.", index);         
      }
  }
}

 

Programın çıktısında görüldüğü üzere oluşturduğumuz 5 adet thread çalışmaya başlamış ve süreçlerine devam edebilmek için “ewHandle” nesnesini beklemektedirler. Her enter tuşuna bastığımızda ilgili bekleyen tek bir thread aktif olmakta ve işini bitirmektedir.

Thread 3 bekliyor.
Thread 2 bekliyor.
Thread 1 bekliyor.
Thread 4 bekliyor.
Thread 5 bekliyor.
Enter'a bastiginizda threadler çalismaya baslayacak.

Thread 4 çalisti ve çikiyor.
Enter'a bastiginizda threadler çalismaya baslayacak.

Thread 3 çalisti ve çikiyor.
Enter'a bastiginizda threadler çalismaya baslayacak.

Thread 5 çalisti ve çikiyor.
Enter'a bastiginizda threadler çalismaya baslayacak.

Thread 1 çalisti ve çikiyor.
Enter'a bastiginizda threadler çalismaya baslayacak.

Thread 2 çalisti ve çikiyor.

Çikmak için ENTER.

Peki “ewHandle” event nesnemizi bekleyen bu threadlerini bu şekilde enter tuş vuruşunu tek tek beklemelerini nasıl sağladık. AutoResetEvent sayesinde.

ewHandle = new EventWaitHandle(false, EventResetMode.AutoReset);

Bu durumda “ewHandle.Set” ile birlikte ewHandle nesnesi SET moduna geçmiş kendisini bekleyen herhangi bir thread çalışmaya başlayınca kendisi otomatik olarak tekrar RESET moduna geri dönmüştür. Bu durumda bekleyen her bir threadin çalışması için bizim tarafımızdan her defasında ewHandle.Set() çalıştırılması gerekti.

 

for (int i = 1; i <= _threadCount; i++)
{
  Console.WriteLine("Enter'a bastığınızda threadler çalışmaya başlayacak.");
  Console.ReadLine();
  ewHandle.Set();
}

Şimdi ise bir önceki örneğimizle hemen hemen aynı olan ama ManualResetEvent kullanan örneğimizi inceleyelim.

using System;
using System.Threading;

namespace Events_ManualResetEvent
{
  public class Example
  {

    private const int _threadCount = 5;
    private static  ManualResetEvent ewHandle;
    public static void Main()
    {
      ewHandle = new ManualResetEvent(false);
      for (int i = 1; i <= _threadCount; i++)
      {
        Thread t = new Thread(
            new ParameterizedThreadStart(ThreadJob));
        t.Start(i);
      }
      Thread.Sleep(500);
      Console.WriteLine("Enter'a bastığınızda bütün threadler çalışmaya başlayacak.");
      Console.ReadLine();
      ewHandle.Set();
      Thread.Sleep(1000);
      Console.WriteLine();
      Console.WriteLine("Çıkmak için ENTER.");
      Console.ReadLine();
    }

    public static void ThreadJob(object threadIndex)
    {
      int index = (int)threadIndex;
      Console.WriteLine("Thread {0} bekliyor.",index);
      ewHandle.WaitOne();
      Console.WriteLine("Thread {0} çalıştı ve çıkıyor.",index);
    }
  }
}

 

Program çıktısında görüleceği üzere 5 adet threadimiz çalışmaya başlamış ve süreçlerine devam edebilmek için ewHandle nesnesini beklemektedirler. Enter tuşuna bastığımızda ewHandle nesnesi SET moduna dönmüş ve ManualResetEvent sınıfından bir nesne olduğu içinse kendisi otomatik olarak RESET moduna dönmemiştir. Bu sayede bekleyen bütün nesneler ewHandle.Set() komutu ile birlikte süreçlerine başlamış ve işlerini bitirmişlerdir.

Thread 3 bekliyor.
Thread 4 bekliyor.
Thread 1 bekliyor.
Thread 5 bekliyor.
Thread 2 bekliyor.
Enter'a bastiginizda bütün threadler çalismaya baslayacak.

Thread 4 çalisti ve çikiyor.
Thread 5 çalisti ve çikiyor.
Thread 1 çalisti ve çikiyor.
Thread 2 çalisti ve çikiyor.
Thread 3 çalisti ve çikiyor.

Çikmak için ENTER.

 

Basitçe bir örnekle event nesnesini bir trafik lambası olarak düşünebilirsiniz. Şeritte bekleyen onlarca araç ve o anda araçların geçmesine izin verebilecek yeşil(SET) ya da kırmızı(RESET) durumdan biri olabilen trafik lambası. Eğer trafik lambası AutoResetEvent modunda ise siz lambayı yeşil(SET)’e çevirdiğinizde o sırada bekleyen tek bir araç geçecek ve akabinde trafik lambası kendisini otomatik olarak kırmızı(RESET) ışık durumuna geçirecektir. ManualResetEvent modunda olan başka bir trafık lambasında siz lambayı yeşil(SET) durumuna soktuğunuzda şeritte bekleyen bütün araçlar geçmeye başlayacak ta ki trafik lambası sizin tarafınızdan tekrar kırmızı(RESET) durumuna dönene dek.

 

Mutex(Mutual exclusion)

            Bilgisayar biliminde Mutex (Mutual exclusion) nesnesi, aynı anda yalnızda tek bir thread/process’in sahip olabilecegi nesnelerdir. Mutex nesnesi herhangi bir thread tarafindan sahip olunduğunda SET modunda uzerindeki kilit kalktığında ise RESET modunda olur. Ayrica mutexlerin bir önemli özelliği işletim sistemi process(süreç)leri arasında geçerliliği olmasıdır. Bu sayede yalnızda tek bir uygulama içerisinde değil işletim sistemi üzerinde koşan birden fazla uygulama arasında paylaşılabilir. Ayrıca mutex’ler üzerinde o mutexe sahip thread birden fazla defa kilit alabilir tabi bu kilidi aldığı kilit kadar serbest bırakmak şartı ile.

Şimdi mutex nesnesinin işletim sistemi üzerinde global bir geçerliliği olmasından faydalanarak çok genel bir örnek yapalım. Uygulamanın o anda çalışıp çalışmadığı kontrolü.

using System;
using System.Threading;

class Program
{
  private static Mutex _lockMtx = new Mutex(false);
  private static int _counter = 0;
  static void Main()
  {
    bool firstInstance;
    Mutex mtx = new Mutex(true,"Test uygulaması 99199",out firstInstance);    
    if (!firstInstance)
    {
      Console.WriteLine("Program zaten çalışıyor.");
      Console.WriteLine("Çıkmak için ENTER");
      Console.ReadLine();
      return;     
    }
    else
    {
      Console.WriteLine("Program çalışmaya başladı.");
    }
    ThreadStart job = new ThreadStart(ThreadJob);
    Thread thread = new Thread(job);
    thread.Start();
    for (int i = 1; i <= 5; i++)
    {
      _lockMtx.WaitOne();
      try
      {
        Console.WriteLine("Ana thread {0} counter değeri {1}",i,_counter);
        _counter++;
        Thread.Sleep(500);
        Console.WriteLine("Ana işçi thread {0} counter değeri {1}",i,_counter);
      }
      finally
      {
        _lockMtx.ReleaseMutex();
      }
    }
    thread.Join();
    Console.WriteLine("Program sonu counter değeri {0}",_counter);
    Console.WriteLine("Program bitti bir tuşa basınız.");
    Console.ReadKey();
  }
  static void ThreadJob()
  {
    for (int i = 1; i <= 5; i++)
    {
      _lockMtx.WaitOne();
      try
      {
        Console.WriteLine("\t\t İşçi thread {0} counter değeri {1}",i,_counter);
        _counter--;
        Thread.Sleep(500);
        Console.WriteLine("\t\t İşçi thread {0} counter değeri {1}",i,_counter);
      }
      finally
      {
        _lockMtx.ReleaseMutex();
      }
    }
  }
}

 

Uygulamayı derledikten sonra çalıştırabilir dosyanın olduğu klasörden uygulamamizi çalıştıralim. Uygulamamız çalışırken aynı dosyayı tekrar çalıştırdığımızda aşağıdaki mesajı alacağız.

Program zaten çalisiyor.
Çikmak için ENTER

Kaynak kodumuzda aşağıdaki satırı inceleyelim.

Mutex mtx = new Mutex(true,"Test uygulaması 99199",out firstInstance);    

Burada mutex sınıfından “mtx”  referansına yeni bir mutex nesnesi yaratılmış. Örneğimizdeki kullanılan constructor’ın parametreleri incelendiğinde

public Mutex(bool initiallyOwned, string name, out bool createdNew);

initallyOwned : Eğer bu parametre true olarak verilirse Mutex’i oluşturan thread mutex oluşturulur oluşturulmaz o mutexin sahibi olacaktır. Örneğimizde bu parametreyi true vermemizin nedeni uygulamamızın ilk çalışan örneğinin bu mutexin sahibi olması böylece aynı anda çalışabilecek diğer örneklerin bu mutex üzerinde sahiplik kuramasisi idi.

name : Mutex’i tanımlayan alfanumerik isim. Bu isimi kullanarak başka processler tarafından oluşturulan mutexlere erişim sağlanır. Örneğimizde uygulamamıza ait bir isim vererek farklı adress space lerde çalışan örneklerin aynı mutexe ulaşabilmesini sağladık.

createdNew : Mutex’i oluştururken eğer o mutex o anda başka bir process tarafından sahiplenilmedi ise bu değer geriye True olarak dönecektir. Eğer mutex başka bir process tarafından sahipli ise bu değer false olarak geri döner.  Örneğimizde dikkat edersiniz bu dönüş değeri üzerine uygulamanın bir örneğinin o anda çalışıp çalışmadığı kontrol edilmiş ve program sonlandırılmıştır.

 

Semaphore
           
Semaphore ortak paylaşılan bir kaynağa belirli sayıda erişim sağlamak amacı ile geliştirilmiş senkronizasyon nesnedir. “Counting Semaphore” olarak adlandırılan(en sık kullanımı bu şekildedir) türünde ortak kullanılacak kaynak sayınız kadar semaphore’u başlatırsınız ve her sahiplenmede(WaitOne) semaphore değeri bir azalır ve semaphore değeri “0” ise sahiplenmeye çalışan thread bloklanır.

Monitor nesnesi ile daha önce yapmış olduğumuz Producer/Consumer(Üretici/Tüketici) uygulamamızı şimdi semaphore ile kullanarak yapalım. Örnek uygulamamızda 4 slotluk bir kuyruk(queue) nesnemiz var ve üretici ilgili slotları doldururken tüketici ilgili slotları boşaltmaktadır. Slotlar dolduğu zaman ise üretici tüketicinin slotlarda en az bir tane slotu boşaltmasını bekleyecek ve boş slot oluştuğunda tekrar o slotu dolduracaktır.

using System;
using System.Collections;
using System.Threading;

namespace Semaphore_ProducerConsumer
{
  class Program
  {
    private static ProducerConsumer queue;
    private static Random rng;
    static void Main()
    {
      queue = new ProducerConsumer(3);
      Thread thread = new Thread(new ThreadStart(ConsumerJob));
      thread.Start();
      rng = new Random(0);
      for (int i = 1; i <= 6; i++)
      {
        Console.WriteLine("Kuyruğa atılan değer {0}", i);  
        queue.Produce(i);
        Thread.Sleep(rng.Next(100));
      }
      thread.Join();
      Console.Write("Program bitti bir tuşa basınız.");
      Console.ReadKey();
    }

 

    static void ConsumerJob()
    {
      Random rng = new Random(1);
      for (int i = 1; i <= 6; i++)
      {
        object o = queue.Consume();
        Console.WriteLine("\t\tKuyruktan alınan değer {0}", o);
        Thread.Sleep(rng.Next(1000));
      }
    }
  }

  public class ProducerConsumer
  {
    readonly object _queueLock;
    Semaphore _smphEmptySlots;
    Semaphore _smphFullSlots;   
    private Queue _queue;
    //Producer/Consumer için slot sayısını(kapasite) belirtiyoruz
    public ProducerConsumer(int capacity)
    {
      //Kapasite kadar bir queue nesnesi oluşturdu.
     _queue = new Queue(capacity);
     //Boş slotlar için bir semaphore
     _smphEmptySlots  = new Semaphore(capacity, capacity);
     //Dolu slotlar için bir semaphore
     _smphFullSlots  = new Semaphore(0,capacity);  
     //Queue nesnemiz üzerinde işlem yaparken kullanılacak bir kilit nesnesi 
     _queueLock = new object();
    }

    public void Produce(object o)
    {
      //_smphEmptySlots semaphore için istekte bulun eğer semaphore değeri 0       //ise thred bloklanacaktır ta ki diğer tüketici slot boşaltana dek. Eğer değer //0 değilse _smphEmptySlots semaphorenun değeri 1 azalacaktır.
      _smphEmptySlots.WaitOne();
      try
      {
//Kuyrukta işlem yapabilmek için kilit al ve kuyruğa değer at.
        lock (_queueLock)
        {
          _queue.Enqueue(o);
        }
      }
      finally
      {
//_smphFullSlots semaphore’unu release et bu durumda semaphore un değeri bir //artacaktır.
        _smphFullSlots.Release();
      }
    }

    public object Consume()
    {
//_smpFullSlots semaphore için istekte bulun. Eğer semaphoreun değeri “0” yani
//dolu slot yok ise bekle ta ki slotlardan biri boşaltılana dek.
      _smphFullSlots.WaitOne();
      try
      {
//Kuyruk üzerinde işlem yapabilmek için kilit al
        lock (_queueLock)
        {
            return _queue.Dequeue();
        }
      }
      finally
      {
//_smphEmptySlots sempahorunun değerini bir arttır bu durumda beklemekte olan //üretici nesnesi var ise uyanacak ve boş slota bir değer yükleyecektir
//(Sleep()  sebebi ile bu işlem Sleep() süresini doldurunca olacaktır)
        _smphEmptySlots.Release();
      }
    }
  }
}

 

Kuyruga atilan deger 1
                Kuyruktan alinan deger 1
Kuyruga atilan deger 2
Kuyruga atilan deger 3
Kuyruga atilan deger 4
                Kuyruktan alinan deger 2
Kuyruga atilan deger 5
Kuyruga atilan deger 6
                Kuyruktan alinan deger 3
                Kuyruktan alinan deger 4
                Kuyruktan alinan deger 5
                Kuyruktan alinan deger 6
Program bitti bir tusa basiniz.

İlgili örnekte görüldüğü üzere elimizde sınırlı sayıda slota sahip bir kuyruğumuz(queue) var ve üretici nesnesi her seferinde bu kuyruğa eleman eklemeye çalışıyor ve her eleman eklediğinde semaphore’lar yardımı ile o kuyruğu boşaltmakla yükümlü tüketici threadini uyariyor ve ilgili thread kuyruğa atılan elemanları tekrar kuyruktan çekiyor ve kuyruktan her bir eleman çekildiğinde ise semaphore’un değerini bir azaltarak diğer thread’i uyarıyor. Eğer üretici thread boş slot kalmadığından dolayı beklemede kaldıysa tüketici thread’inin her slot boşaltmasında sinyal alıyor ve “_smphEmptySlotssemaphore’unu bekleyerek boş slot oluştu ise tekrar slot doldurma işine devam ediyor.  İlgili örnek daha öncede değindiğimiz üzere process’ler arası mesajlaşma ve mesaj kuyruğu uygulamasına dair basit bir örnek olabilir.