Makale Özeti

Kısaca Multithreading programcıların uygulama geliştirirken yazdıkları kod parçalarını eş zamanlı olarak sistem üzerinde çalıştırabilme mekanizmasına denir. Bu mekanizma sayesinde uzun zaman gerektiren işlemlere kullanıcının bilgisayar ile iletişimi kesintiye uğramadan ya da uzun süreli çalışması gereken uygulamanın o an işletim sistemi üzerinde koşan diğer uygulamaları engellemeden arka planda çalışabilme özelliği katılır. Örneğin oldukça uzun süren bir raporu ayrı bir thread içerisine aldığınızda rapor arka planda hazırlanırken kullanıcı kullandığı uygulama içerisinde işlemlere devam edebilir.

Makale

C#.NET ve Threading (Temel kavramlar ve nesneler)

Temel Kavramlar

 

Kısaca Multithreading programcıların uygulama geliştirirken yazdıkları kod parçalarını eş zamanlı olarak sistem üzerinde çalıştırabilme mekanizmasına denir. Bu mekanizma sayesinde uzun zaman gerektiren işlemlere kullanıcının bilgisayar ile iletişimi kesintiye uğramadan ya da uzun süreli çalışması gereken uygulamanın o an işletim sistemi üzerinde koşan diğer uygulamaları engellemeden arka planda çalışabilme özelliği katılır. Örneğin oldukça uzun süren bir raporu ayrı bir thread içerisine aldığınızda rapor arka planda hazırlanırken kullanıcı kullandığı uygulama içerisinde işlemlere devam edebilir.

.Net framework içerisinde threading uygulamalar ve threading mekanizmasını incelemeden önce bazı kavramlar üzerinden kısaca da olsa durmamız gerekiyor.

 

Multitasking : MS-Windows, GNU/Linux, MacOSX, Haiku ve diğer modern işletim sistemleri multitasking yapabilen işletim sistemleri olarak tanımlanır. Multitasking, işletim sisteminin o an sistem üzerinde koşan processlerden sırasını geleni(scheduling) belirlenmiş her bir zaman aralığında(quantum, quanta, time slice…) çalıştırması ve böylece process’ler arası geçişler yaparak(context switching, task switching) sistem üzerinde gerçek anlamda eş zamanlı olmasa bile eş zamanlı olarak uygulamaları çalıştırmasıdır. Buradaki eş zamanlılık kavramı işletim sistemi ve üzerinde koştuğu donanım ile bağlantılıdır. Örneğin sadece tek bir cpu’nun (artık tek bir çekirdek demeliyiz) olduğu sistemlerde eş zamanlılık kavramsal olarak anlaşılırken üzerinde 2 ya da daha fazla cpu bulunan multiprocessor sistemlerde fiziki olarak aynı anda iş parçacıkları ayrı cpu’lara dağıtılarak gerçek eş zamanlılık kazanılabilir.

Process(süreç) : Sanıyorum bu tanımla MS-Windows işletim sistemi kullanıcılarının aklına hemen görev yöneticisi(task manager) uygulamasında görülen ve genel tabiriyle programlar dediğimiz process(süreç)ler gelmiştir. Yanlış bir tanım değil sadece biraz daha açmamız gerekiyor. Process(süreç) kavramı işletim sistemi üzerinde koşan ve kendisine ait ve diğer process’ler tarafından ulaşılması engellenmiş hafıza(memory) ve kaynakları olan kod parçasıdır. Hafıza ise stack(yığın), data ve kod bölümden oluşur.

Thread : Thread kavramı, bir process’e bağlı daha doğrusu bir process tarafından oluşturulmuş o process’in adres uzayını(address space) ve kaynaklarını kullanabilen ama bununla birlikte kendisine ait bir stack ve mesaj kuyruğuna(message queue) sahip kod parçasıdır. Bir process in çalışmaya başlaması ile birlikte bir thread oluşturulur(Ana thread ya da Main thread) ve bu process içerisinde programcının direktifleri ile birden fazla thread oluşturulabilir(İşçi thread ya da Worker thread).

Priority Level(Öncelik seviyesi) : Multitasking sistemlerde process ya da threadlerin çalışması boyunca scheduler mekanizmasına ilgili process ya da thread’in hangi öncelikte çalıştırılacağı daha doğrusu o thread’in diğer threadlere oranla önceliği belirtilir. Yüksek öncelikli threadler düşük öncelikli olanlara oranla daha sık çalıştırılır. Düşük öncelikli threadler sistem aktivite ve benzeri izlemeleri(file monitor, garbage collector…) yapmak için kullanılırken yüksek öncelige sahip thread’ler kesintiye uğraması kabul edilemeyen zaman kritik(time-critical) işlemler için kullanılır. Örneğin bir donanım ile haberleşen ve o cihazı kontrol eden süreçler. Bunların dışında zaten örneklerimizde çalışacağımız gibi normal önceliğe sahip threadler kullanılır.

 

Scheduling : Scheduling mekanizması işletim sistemi içerisinde processler ve bağlı thread’lerin öncelik sıralarının atanmasından sorumlu mekanizmayı tanımlar. Scheduler uygulaması Scheduling olarak bilinen algoritmalar kullanılarak geliştirilirler. Örneğin Ms-Windows “round robind scheduling” algoritmasını kullanarak process’ler arası önceliği sağlar.

Context switching : Multitasking sistemlerde işletim sistemi o anda kendi üzerinde koşan processleri belli bir zaman aralığında çalıştırmak ve bunlar arasında geçiş yapmakla yükümlüdür. Belli bir quanta’da çalışan process’i hafızadan alıp onun yerine sıradaki process’i hafızaya yükleyip çalıştırmakla yükümlü mekanizma context switching ya da task switching olarak tanımlanır.

 

Quanta, Quantum, Time slice : Cpu üzerinde bir process’in ne kadar süre çalışabileceği bu sabit ile belirlenir. Örneğin minix üzerinde bu rakam “40 ms.”(tam emin değilim) MS-Windows ve diğer modern işletim sistemlerinde bu rakam sistem yöneticisi tarafından ayarlanabilir. Özetlemek gerekirse context switching sayesinde o an çalışan process tüm memory(stack, data, ip,cs…)bilgileri ile hafızadan alınır ve sıradaki process hafızaya yüklenerek bir quanta kadar çalıştırılır ve tekrar context switching devreye girerek schedulerden bir sonraki process’i alir ve bu süreç devam eder.

 

.Net ve threading

            .NET içerisinde threading mekanizmasını kullanmak için uygulamanıza System.Threading ad uzayını(namespace) eklemeniz gerekiyor.Aşağıda en basit hali ile threading mekanizmasını gösterelim.

 

//İlk olarak System.Threading.Thread sınıfında bir nesne tamılıyoruz.
System.Threading.Thread thread;
//Tanımladığımız thread nesnesini oluşturuyoruz. Ve işte burda nesnemizi
 //oluştururken arkaplanda çalışmasını istediğimiz metotumuzu parametre olarak
//geçiyoruz.
//Bu herhangi bir nesnenin metotu olabileceği gibi kendi sınıfınıza ait bir
// metot olabilir.
thread = new System.Threading.Thread(anObject.AMethod);
//Ve geriye Start() metotu ile kodu çalıştırmak kalıyor.
thread.Start();

Örnekte verdiğimiz üzere her şey bu kadar basit ama başlangıç için. Şimdi bu bilgiler ile basitçe ilk programımızı hazırlayalım. Multithreading ile oldukça karmaşık çözümlerde hazırlayabiliriz fakat bu doküman da en genel geçer örneklerden hareket edeceğiz.

 

using System;
using System.Threading;

namespace BasicThreading
{
  class Program
  {
    static void Main()
    {
      Thread thread = new Thread(ThreadJob);
      thread.Start();
      for (int i = 1; i<=5; i++)
      {
        Console.WriteLine("Ana thread {0}", i);
        Thread.Sleep(1000);
      }
      Console.WriteLine("Program bitti bir tuşa basınız.");
      Console.ReadKey();
    }
    static void ThreadJob()
    {
      for (int i = 1; i <= 5; i++)
      {
        Console.WriteLine("\t\tİşçi Thread {0}", i);
        Thread.Sleep(500);
      }
    }
  }
}

Programı çalıştırdığımızda karşımıza gelecek ekranda ise çıktı aynı aşağıda göründüğü gibi olacaktır. Ana thread çalışırken arkaplanda oluşturduğumuz thread de çalışmaktadır.

Ana thread 1
                Isçi Thread 1
                Isçi Thread 2
                Isçi Thread 3
Ana thread 2
                Isçi Thread 4
                Isçi Thread 5
Ana thread 3
Ana thread 4
Ana thread 5
Program bitti bir tusa basiniz.

           
Aslında yazdığınız her uygulama bir threadden oluşur. Uygulamanız “ana thread”(main thread) olarak tabir edilen bu thread içerisinde çalışır. Örneğin yukarıda verdiğimiz ilk örnekteki ana thread main() metotu ile başlar ve biter. Uygulamanız boyunca oluşturacağınız threadler ise “işçi thread”(worker thread) olarak adlandırılıyor.

Sleep(), ThreadStart() ve Join()

            İlk örneğimizde Thread.Sleep(1000) metodunu fark etmiş olmalısınız. Sleep() metodu o anki aktif olan thread’i parametre olarak verdiğiniz milisaniye kadar duraklatır. İlk uygulamamızda ana thread içerisinde program her bir döngüde “1000 ms.” duraklarken işçi thread içerisinde ise “500 ms.” de bir duraklamaktadır.

Yeni uygulamamızı incelemeden önce ilk örnekteki uygulamamız üzerinde biraz değişiklik yapalım. Ana döngüdeki bekleme süresini “500 ms.” işçi thread’in bekleme süresini ise “1000 ms.” yapalım.

........   
    static void Main()
    {
      Thread thread = new Thread(ThreadJob);
      thread.Start();
      for (int i = 1; i<=5; i++)
      {
        Console.WriteLine("Ana thread {0}", i);
        Thread.Sleep(500);
      }
      Console.WriteLine("Program bitti bir tuşa basınız.");
      Console.ReadKey();
    }
    static void ThreadJob()
    {
      for (int i = 1; i <= 5; i++)
      {
        Console.WriteLine("\t\tİşçi Thread {0}", i);
        Thread.Sleep(1000);
      }
    }

           
            Programımızı ilgili şekilde düzeltikten sonra programı çalıştırıp çıktısını tekrar kontrol edelim.
           

Ana thread 1
                Isçi Thread 1
Ana thread 2
                Isçi Thread 2
Ana thread 3
Ana thread 4
Ana thread 5
                Isçi Thread 3
Program bitti bir tuşa basınız.
                Isçi Thread 4
                Isçi Thread 5

            Farkettiğiniz üzere Main() metodu içerisinde programımızın çalışması son satıra kadar geldi ve ekrana “Program bitti bir tuşa basınız.” mesajını yazarak programın sonlanması için bir tuş vuruşu bekledi fakat aynı zamanda işçi thread çalışmaya devam etti. Bir önceki uygulamamızda böyle bir sorunumuz yoktu çünkü işçi thread ana thread den daha hizli çalışıyordu fakat biz son yaptığımız düzeltme ile durumu tersine çevirdik. Peki bu sorunu nasıl çözeriz. İkinci uygulamamızı yazalım.

using System;
using System.Threading;

namespace BasicThreading2
{
  class Program
  {
    static void Main()
    {
      Worker worker = new Worker();
      ThreadStart job = new ThreadStart(worker.DoWork);
      Thread thread = new Thread(job);
      thread.Start();
      for (int i = 1; i <= 5; i++)
      {
        Console.WriteLine("Ana thread {0}",i);
        Thread.Sleep(500);
      }
      thread.Join();//Worker thread işini bitirene kadar bekle.
      Console.WriteLine("Program bitti bir tuşa basınız.");
      Console.ReadKey();
    }
  }
  public class Worker
  {
    public void DoWork()
    {
      for (int i = 1; i <= 5; i++)
      {
        Console.WriteLine("\t\tİşçi Thread {0}",i);
        Thread.Sleep(1000);
      }
    }
  }
}

Programı çalıştıralım, program çıktısında göreceğiniz gibi programın sonlandırılması için işçi thread’in kodunu çalıştırması ve işini bitirmesi beklenmektedir.

Ana thread 1
                İşçi Thread 1
Ana thread 2
Ana thread 3
                İşçi Thread 2
Ana thread 4
Ana thread 5
                İşçi Thread 3
                İşçi Thread 4
                İşçi Thread 5
Program bitti bir tuşa basınız.

İşte bu işçi thread’in işini bitirmesini Join() metodu ile sağlıyoruz.

thread.Join();//Worker thread işini bitirene kadar bekle.

Join metodu çağrıldığında çağıran thread join metodu çağrılmış threadin işini bitirmesine kadar bekler ve thread işini bitirdiğinde bir sonraki sözdizimi(statement) itibari ile çalışmasına devam eder. Buradaki örnekte olduğu gibi ana thread işçi threadin join metodunu çağırır ve işçi thread işini bitirene kadar bloke kalır. Bu sayede işçi threadlerin uygunsuz kapatılmasından dolayı oluşabilecek problemlerin önüne geçebiliyoruz ve kullandıkları kaynakları serbest bırakabilmelerini sağlıyoruz.

Örnek programımız içerisinde yeni bir şey farkettiniz. ThreadStart.

public delegate void ThreadStart();

Tanımda görüldüğü üzere ThreadStart aslında bir delegedir(delegate). İlk örneğimizde İşçi thread’e çalıştırması için bir metot vermiştik fakat ikinci örneğimizde ThreadStart ile birlikte çalıştırılmasını istediğimiz kodu ThreadStart delegesi ile paramatre olarak thread nesnemize geçtik. Bundan sonra Thread nesnesine geçecegimiz kodları ThreadStart ile geçeçeğiz. (Bu yontemin kullanılmasını tavsiye ederim bu sayede .net içerisinde delegeler ile reflection mekanizmasindan sayede faydalanabiliriz.)

ParameterizedThreadStart

            İlk iki örneğimizde thread nesnelerimiz ile arkaplanda kodlarımızı çalıştırdık ama thread nesnelerimize bir başlangıç parametresi vermek isteseydik? İşte bu durumda ParameterizedThreadStart delegesini kullanacağız.

public delegate void ParameterizedThreadStart(object obj);

           
            Tanımda görüldüğü üzere  ParameterizedThreadStart aynı ThreadStart gibi bir delegedir ama parametre olarak bir object nesnesi alır. Bu sayede threadimize parametre geçebiliyoruz.

Aşağıdaki kodumuzu inceleyelim.

using System;
using System.Threading;

namespace BasicThreading2
{
  class Program
  {
    static void Main()
    {
      Worker worker = new Worker();
      ParameterizedThreadStart threadstart = new                                                                           ParameterizedThreadStart(worker.DoWork);
      Thread thread = new Thread(threadstart);
      thread.Start(10);
      for (int i = 1; i <= 5; i++)
      {
        Console.WriteLine("Ana thread {0}",i);
        Thread.Sleep(500);
      }
      thread.Join();//Worker thread işini bitirene kadar bekle.
      Console.WriteLine("Program bitti bir tuşa basınız.");
      Console.ReadKey();
    }
  }
  public class Worker
  {
    public void DoWork(object count)
    {
      for (int i = 1; i <= (int)count; i++)
      {
        Console.WriteLine("\t\tİşçi Thread {0}",i);
        Thread.Sleep(1000);
      }
    }
  }
}

Program çıktısı

Ana thread 1
                İşçi Thread 1
Ana thread 2
Ana thread 3
                İşçi Thread 2
Ana thread 4
Ana thread 5
                İşçi Thread 3
                İşçi Thread 4
                İşçi Thread 5
                İşçi Thread 6
                İşçi Thread 7
                İşçi Thread 8
                İşçi Thread 9
                İşçi Thread 10
Program bitti bir tuşa basınız.

Program çıktısında gördüğünüz üzere İşçi thread “10” a kadar saydı. İşçi thread’e kendisini Start etmeden önce kaça kadar sayması gerektiğini parametre olarak verdik. thread.Start(object parameter) burada görüldüğü üzere Start a vereceğiniz object tipinden parametre sizin ParameterizedThreadStart delegenize aynen geçirilir ve bu delegeye uygun tanımladığınız metot içerisinde ilgili parametreyi kullanabilirsiniz.

 

Race condition

            Race Condition, process’lerin ortak bir kaynak üzerinde işlem yapmaları sonucu ilgili kaynakta meydana gelen tutarsızlık olarak tanımlanabilir. Bir örnekle açıklarsak elimizde 2 adet threadimiz(t1, t2) olsun ve bu threadler birlikte ortak kullandıkları (i) değerini 1 arttırsınlar.

i = 0;
            t1 i değerini temp1 alanına okur i değeri 0
            t1 temp1 alanını 1 arttirir sonuc 1
            t1 temp1 alanındaki değeri i ye taşır i değeri 1
t2 i değerini temp2 alanına okur i değeri 1
t2 temp2 alanını 1 arttır sonuc 2
t2 temp2 alanındaki deperi i ye taşır i değeri 2
            işlem sonucu i = 2

Beklenen sonucumuz “2” idi ve bu programda “2” değerini elde ettik. Yalnız programımız yukarıdaki gibi değil de aşağıdaki gibi çalışsa idi.

i = 0;
            t1 i değerini temp1 alanına okur i değeri 0
            t1 temp1 alanını 1 arttir sonuc 1
t2 i değerini temp2 alanına okur i değeri 0
            t1 temp1 alanındaki değeri i ye taşır i değeri 1
t2 temp2 alanını 1 arttır sonuc 1
t2 temp2 alanındaki deperi i ye taşır i değeri 1
            işlem sonucu i = 1//Race condition

Görüldüğü gibi threadlerimiz birbirlerinin kullandıkları ortak bir değişkene olmamaları gereken bir anda müdahale ettiler ve “2” olması gereken sonucumuz “1” oldu yani Race Condition oluştu ya da kısaca Data Race. Saniyorum Race Condition burada daha net anlaşılmıştır.

Şimdi race condition oluşacak örnek bir program yapalım. Bu uygulamada Ana thread integer tipindeki “counter” sayacımızı “1” fazla arttırır iken işçi threadimizde “1” kadar azaltacaktır.

Sonuçta counter değişkenimize atadığımız başlangıç değeri ne ise program bitiminde aynı değere geri dönmeyi umuyoruz.

using System;
using System.Threading;
namespace DataRace
{
  class Program
  {
    private static int counter;
    static void Main()
    {
      ThreadStart job = new ThreadStart(ThreadJob);
      Thread thread = new Thread(job);
      thread.Start();
      for (int i = 1; i <= 5; i++)
      {
        Console.WriteLine("Ana thread {0} counter değeri {1}",i,counter);     
        int j = counter;
        j++;
        Console.WriteLine("Ana thread {0} j değeri {1}",i,j);       
        Thread.Sleep(1000);       
        counter = j;
        Console.WriteLine("Ana thread {0} counter değeri {1}",i, counter);
      }
      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++)
      {
        Console.WriteLine("\t\t İşçi thread {0} counter değeri {1}",i,counter);
        int j = counter;
        j--;
        Console.WriteLine("\t\t İşçi thread {0} j değeri {1}",i,j);               
        Thread.Sleep(500);       
        counter = j;
        Console.WriteLine("\t\t İşçi thread {0} counter değeri {1}",i,counter);
      }
    }
  }
}

Program çıktısına bakalım

Ana thread 1 counter değeri 0
Ana thread 1 j değeri 1
                 İşçi thread 1 counter değeri 0
                 İşçi thread 1 j değeri -1
                 İşçi thread 1 counter değeri -1
                 İşçi thread 2 counter değeri -1
                 İşçi thread 2 j değeri -2
Ana thread 1 counter değeri 1
Ana thread 2 counter değeri 1
Ana thread 2 j değeri 2
                 İşçi thread 2 counter değeri -2
                 İşçi thread 3 counter değeri -2
                 İşçi thread 3 j değeri -3
                 İşçi thread 3 counter değeri -3
                 İşçi thread 4 counter değeri -3
                 İşçi thread 4 j değeri -4
Ana thread 2 counter değeri 2
Ana thread 3 counter değeri 2
Ana thread 3 j değeri 3
                 İşçi thread 4 counter değeri -4
                 İşçi thread 5 counter değeri -4
                 İşçi thread 5 j değeri -5
                 İşçi thread 5 counter değeri -5
Ana thread 3 counter değeri 3
Ana thread 4 counter değeri 3
Ana thread 4 j değeri 4
Ana thread 4 counter değeri 4
Ana thread 5 counter değeri 4
Ana thread 5 j değeri 5
Ana thread 5 counter değeri 5
Program sonu counter değeri 5
Program bitti bir tuşa basınız.

           Görüldüğü gibi program çıktımız “0” olması gerekirken “5” olarak karşımıza çıktı. Peki bunun çözümü nedir? Senkronizasyon.

Senkronizasyon

            Önceki “data race” örneğinde gördüğümüz gibi threadlerin ortak bir kaynağa rastgele erişmesi ve üzerinde işlem yapması sonucu program akışının tutarlılığını kaybetmiştik. İşte bu sorunların önüne geçebilmek için threadler arası uyum mekanizmasına thread Senkronizasyonu diyoruz.

İşletim sistemi ve threading altyapısı sağlayan frameworkler senkronizasyon sağlamak için bazı nesneler sunarlar(Critical section, Mutex, Semaphore, Event, Monitor…).net framework ile birlikte gelen senkronizasyon nesneleri incelemeden önce başka bir tanımı açıklayalım “Thread Safe”.

Thread Safe : Thread safe birden fazla thread tarafından kullanıldığında tutarlılığını kaybetmeyen kod  parçalarıdır. En basit tarifi ile yazdığınız ve sadece kendisine ait değişkenleri kullanan bir metot yada fonksiyon thread safe dir. Çünkü ilgili metot kendisine ait değişkenler ve “call by value” olarak verilmiş argümanlar için stack’de yer açacak ve işi bitince boşaltacaktır bu durumda bu metot aynı anda birden fazla thread tarafından çağrıldığında herhangi bir tutarsızlık oluşmayacaktır. Önceki örnek programımız thread safe değildi bu sebepten iki ayrı thread tarafından ortak kullanılan değişkenimizde tutarsızlık oluşmuştu. Birazdan senkronizasyon nesnelerini kullanarak thread safe kod geliştirmeye başlayacağız.

Kilit nesneleri (Critical section, Monitor, Lock)

MS-Windows işletim sistemi win32 api seti ile birlikte programcılara kilit mekanizması olarak Critical section yapısı sunar. .net framework içerisine ise aynı işi yapmak için kullanımı basitleştirilmiş ama threadler arası sinyal gönderimi yeteneği eklenmiş Monitor nesnesi ve c# dili için lock statement’i eklenmiştir. Bu kilit mekanizmalari(ben bu şekilde isimlendiriyorum) daha sonra anlatacağımız senkronizasyon nesnelerinden farklıdır. Critical section ve lock ile threadler arası sinyal gönderimi yapılamaz ve bu nesneler yalnızca tek bir process içerisinde o processe bağlı threadler tarafından kullanılabilir.
Monitor nesnesi ise critical section(.net framework içerisinde critical section direk uygulanmamıştır) yerine kullanılır ve yukarıda değindiğimiz gibi threadler arası sinyalleşme yeteneğine sahiptir.  Bu nesnelerin temel amacı çalışması gereken bir kod bloğuna kapı görevi görmektir. Aynı anda sadece tek bir thread ilgili kod bloğuna girebilir ve aynı kod bloğuna gelen diğer threadler ise bloğa girebilmek için bloğu kilitleyen threadin kilidi bırakmasına kadar beklerler. Bununla birlikte bu kilit mekanizması uygulamanızın performansını doğal olarak olumsuz yönde etkileyecektir. Uygulamanız içerisinde görevleri ne kadar çok threade dağıtmış olursanız olun aynı kod bloğunu kullanmaları gerektiğinde sadece bir tanesi çalışabilecek ve aynı bloğu bekleyen diğer threadler beklemede kalacaktır. Multithreading ve senkronizasyon konularında tecrübeniz arttıkça bu tarz problemlerin önüne geçebilmek için gerekli çözümleri görebileceksiniz.

Şimdi biraz önceki içerisinde datarace oluşan uygulamamıza gerekli tedbirleri ekleyelim.

using System;
using System.Threading;
namespace Monitors
{
  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++)
      {
        Monitor.Enter(_lock);
        try
        {
          Console.WriteLine("Ana thread {0} counter değeri {1}",i, _counter);
          int j = _counter;
          j++;
          Console.WriteLine("Ana thread {0} j değeri {1}",i,j);
          Thread.Sleep(1000);
          _counter = j;
          Console.WriteLine("Ana thread {0} counter değeri {1}",i, _counter);
        }
        finally
        {
          Monitor.Exit(_lock);
        } 
      }
      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++)
      {
        Monitor.Enter(_lock);
        try
        {
          Console.WriteLine("\t\t İşçi thread {0} counter değeri {1}",i,_counter);
          int j = _counter;
          j--;
          Console.WriteLine("\t\t İşçi thread {0} j değeri {1}",i,j);
          Thread.Sleep(500);
          _counter = j;
          Console.WriteLine("\t\t İşçi thread {0} counter değeri {1}",i,_counter);
        }
        finally
        {
          Monitor.Exit(_lock);
        } 
      }
    }
  }
}

Program çıktımız aşağıdaki gibi olacak ve beklediğimiz “0” değerini elde etmiş olacağız.

Ana thread 1 counter değeri 0
Ana thread 1 j değeri 1
Ana thread 1 counter değeri 1
                 İşçi thread 1 counter değeri 1
                 İşçi thread 1 j değeri 0
                 İşçi thread 1 counter değeri 0
Ana thread 2 counter değeri 0
Ana thread 2 j değeri 1
Ana thread 2 counter değeri 1
                 İşçi thread 2 counter değeri 1
                 İşçi thread 2 j değeri 0
                 İşçi thread 2 counter değeri 0
Ana thread 3 counter değeri 0
Ana thread 3 j değeri 1
Ana thread 3 counter değeri 1
                 İşçi thread 3 counter değeri 1
                 İşçi thread 3 j değeri 0
                 İşçi thread 3 counter değeri 0
Ana thread 4 counter değeri 0
Ana thread 4 j değeri 1
Ana thread 4 counter değeri 1
                 İşçi thread 4 counter değeri 1
                 İşçi thread 4 j değeri 0
                 İşçi thread 4 counter değeri 0
Ana thread 5 counter değeri 0
Ana thread 5 j değeri 1
Ana thread 5 counter değeri 1
                 İşçi thread 5 counter değeri 1
                 İşçi thread 5 j değeri 0
                 İşçi thread 5 counter değeri 0
Program sonu counter değeri 0
Program bitti bir tuşa basınız.

Göreceğiniz gibi bir önceki uygulamadan farklı olarak her bir thread işlemini yaparken diğer threadin o sırada kullandığı kaynağa erişimini engelledi kısaca işleri atomikleştirdi(atomicity).

Atomicity
            Yazılım mühendisliginde kesintiye uğramadan bir bütün olarak çalışacak işlemlere Atomic işlemler denir. Örneğin veritabanındaki bir tabloya kayıt eklemek atomic bir işlemdir. O sırada oluşacak bir hatada yarım işlenmiş bir satır olmayacaktır.

Peki ortak kaynağa ulaşımı diğer threadlere nasıl kapattık? Örnek uygulamada Monitor nesnesi ile.

Programı daha detaylı inceleyelim.

    //Kilit mekanizması için bir object yaratıyoruz.
    private static readonly object _lock = new object();
    static void Main()
      for (int i = 1; i <= 5; i++)
      {
        //Monitor.Enter metodu ile birlikte _lock nesnemizi kilitliyoruz.
        //Eğer o an _lock nesnesi üzerinde bir kilit var ise kilit
        //kaldırılana kadar bekliyoruz. Girdiğimizde ise _lock nesnesini
        //bizim tarafımızdan kilitlenmiş olacak.                                                                                                                             
        Monitor.Enter(_lock);
        try
        {
          ...
          ...
          ...
        }
        finally
        {
          Monitor.Exit(_lock);
          //_lock nesnesi üzerindeki kilidimizi kaldırdık.
        } 
      }
      ...
      ...
      ...
    static void ThreadJob()
    {
      for (int i = 1; i <= 5; i++)
      {
        //Aynı şekilde kilit için bekliyoruz
        Monitor.Enter(_lock);
        try
        {
         //Kilidi aldık
          ...
          ...
          ...
        }
        finally
        {
          Monitor.Exit(_lock);
          //İşimiz bitti kilidi kaldırabiliriz.
        } 
      }
    }

Örnek uygulamada gördüğünüz gibi, Thread “_lock” nesnesi için kilit aldı ve işini bitirince “_lock” üzerindeki kilidi serbest bıraktı. İşte bu kilit alma ve serbest bırakma süresince sadece bu aralıktaki kodumuzun çalışmasını ve diğer threadlerin bizi beklemesini sağladık.

Kullandığımız Monitor nesnesi gibi C# bize ayrıca lock() deyimide sağlar. lock() deyimi Monitor nesnesinden farklı olarak bir blok olarak kullanılır ve blok sonunda kilit alınan nesne üzerinden kendisi otomatik olarak kilidi kaldırır. Örnekte görelim

using System;
using System.Threading;
namespace LockStatement
{
  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++)
      {
        lock(_lock)
        {
          Console.WriteLine("Ana thread {0} counter değeri {1}",i,_counter);
          int j = _counter;
          j++;
          Console.WriteLine("Ana thread {0} j değeri {1}",i,j);
          Thread.Sleep(1000);
          _counter = j;
          Console.WriteLine("Ana thread {0} counter değeri {1}",i,_counter);
        }
      }
      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++)
      {
        lock(_lock)
        {
          Console.WriteLine("\t\t İşçi thread {0} counter değeri {1}",i,_counter);
          int j = _counter;
          j--;
          Console.WriteLine("\t\t İşçi thread {0} j değeri {1}",i,j);
          Thread.Sleep(500);
          _counter = j;
          Console.WriteLine("\t\t İşçi thread {0} counter değeri {1}",i,_counter);
        }
      }
    }
  }
}

 

lock(_lock)//Nesne üzerinde kilit al yada alanan kadar bekle
{
  ...
  ...
}//Blok sonunda otomatik olarak kilidi bırak

lock deyimi sayesinde finally bloğunda ya da işimiz bitince nesne üzerindeki kilidi bırakma zorunluluğumuz ortadan kalktı.

DEADLOCKS (Kilit Çıkmazları)

            Multithread program yazmak belli bir birikimden sonra her ne kadar kolay olsada dikkat edilmesi gereken çok önemli bir noktayıda beraberinde getirir. DeadLocks. Nedir bu DeadLock? Bunu örnek bir uygulama yaparak gösterelim.

using System;
using System.Threading;

namespace DeadLock
{
  class Program
  {
    static readonly object _lock1 = new object();
    static readonly object _lock2 = new object();
    static void Main()
    {
        new Thread(new ThreadStart(ThreadJob)).Start();
        Thread.Sleep(500);
        Console.WriteLine ("Lock2 kilitleniyor");
        lock (_lock2)
        {
            Console.WriteLine ("Lock2 Kilitlendi");
            Console.WriteLine ("Lock1 kilitleniyor");
            lock (_lock1)
            {
                Console.WriteLine ("Lock1 kilitlendi");
            }
            Console.WriteLine ("Lock1 kilidi iptal ediliyor");
        }
        Console.WriteLine("Lock2 kilidi iptal ediliyor");
    }

    static void ThreadJob()
    {
        Console.WriteLine ("\t\tLock1 kilitleniyor");
        lock (_lock1)
        {
            Console.WriteLine("\t\tLock1 kilitlendi");
            Thread.Sleep(1000);
            Console.WriteLine("\t\tLock2 kilitleniyor");
            lock (_lock2)
            {
                Console.WriteLine("\t\tLock2 kilitlendi");
            }
            Console.WriteLine ("\t\tLock2 kilidi iptal ediliyor");
        }
        Console.WriteLine("\t\tLock2 kilidi iptal ediliyor");
    }
  } 
}

            Şimdi programımızın çıktısına bir bakalım


                Lock1 kilitleniyor
                Lock1 kilitlendi
Lock2 kilitleniyor
Lock2 Kilitlendi
Lock1 kilitleniyor
                Lock2 kilitleniyor

Görüldüğü gibi, ilk olarak İşçi threadimiz “_lock1” üzerinde kilit alırken ana thread ise “_lock2” üzerinde kilit aldı. Daha sonraki adımda ise ana thread koda devam edebilmek için işçi thread tarafından kilitlenmiş “_lock1” üzerinde kilit almaya çalıştı ama “_lock1” zaten işçi thread tarafından kilitlendiği için mecburen işçi thread kilidi serbest bırakana kadar beklemek zorunda kaldı. Aynı zamanda işçi thread zaten ana thread tarafından kilitlenmiş olan “_lock2” üzerinde kilit almaya çalışınca zaten üzerinde kilit bulunan bu nesneyi beklemeye başladı ve sonuç “DeadLock”. İki thread birbirini beklemeye koyuldu ve biz ctrl+c tusuna basıp programı sonlandırana kadarsa beklemeye devam edecekler.

            Deadlock kavramı program içerisinde yakalanmasi çok zor bir bugdır. Bundan dolayı multithreading uygulamalarda olabildiğince dikkatli kod yazmanızı öneririm.