Task Parallel Library TLP C#

24 Ara

İşlemci dünyasının gelişimi ve değişimi, işlemcilerin çekirdek sayısı transistör sayısını ne zaman yakalar diye düşündürüyor insana. Bilgisayarlarımızın işlemcileri geliştirkçe programlarımızın daha da hızlandığı aşikardır. Ancak programların hızlanması sadece bilgisayarların fiziksel özellikleriyle alakalı bir durum değildir. Diğer taraftan yazılımların da işlemcileri iyi kullanabilecek şekilde hazırlanmaları gerekmektedir.

İşlemci çekirdekleri arttıkça uzun sürebilecek işlemleri parçalar halinde bölüştürerek işlemcileri tam kapasitede kullanabilmek işimizi daha çok hızlandıracaka ve kolaylaştıracaktır.

Bu yazımızda da paralel işlemlerin alt yapısını oluşturan Task Parallel Library (TLP) konusunu incelemeye çalışacağız.

TPL kavramını .Net Framework 4.0 ile tanımaya başlıyoruz. Bu yapı, işlemlerin paralel bir şekilde yürütülmesini sağlamak amacıyla oluşturulmuştur. TLP’ye ait tipler System.Threading ve System.Threading.Tasks isim alanında bulunmaktadır.

TLP altyapısı aslında süreçlere, görevler olarak bakar ve tıpki insanların görevleri yütebildiği mantıkla organize edilebilmektedir.

  • Yeni görevler oluşturmak, bu görevleri başlatmak, duraklatmak ve sonlandırmak mümkündür.
  • Bir görevin bittiği yerden başka bir görevi başlatmak mümkündür.
  • Başarıyla yerine getirilen görevlerin sonucunda değerler döndürmek mümkündür.
  • Bir görev kendi içinde alt görevler başlatabilir.
  • Görevler aynı veya farklı thread’ler tarafından yerine getirilebilirler.

Task(görev), başarılı bir şeklide tamamlanmasını istediğimiz bir süreçtir. Süreç T1 zamanında başlar ve T2 zamanında sonlanır.

Bu sürecin tamamlanma süresi, işlemcimizin özelliklerine ve kod yazım biçimlerine göre değişebilir.

Thread(ler) ise görevleri yerine getiren işçilerdir. Bu işçiler bir veya birden fazla olabilir. Her görev ayrı bir thread tarafından yürütülmek zorunda değildir. Bir thread birden fazla görevi yerine getirebilir. Ya da bir görev birden fazla thread(multithread) yardımıyla yapılabilmektedir.

Paralel programlama her problemin çözümü için hızlı bir çözüm olmayabilir. Paralel işlemler ile çözüme birden fazla thread yardımıyla yaklaşmış oluyoruz. Örneğin bir apartmanın en üst katına 2 tuğla çıkarmak için bört kişi çağırıp 1 tuğlayı iki kişiye tutturup çıkartmak mantıklı değildir ve maliyetlidir. Bu işi bir kişi gayet seri bir şeklide yapabilir ve uzucdur. Ancak 100 tuğla olduğunda bir kişi  bu işte çok yorulacağından maliyet fazladır. Bu durumda 100 tuğla için dört kişi çağırmak mantıklıdır ve maliyetleri azaltır.

Yazılımsal olarak gelişmiş bir örnek üzerinden giderek sonuca bakacak olursak, paralel ve seri işlemlerin farkını daha rahat görebiliriz.

Bu örneğimizin senaryosu istatistiksel bir formül olan Standart Normal Dağılım’ın matematiksel formülünü döngü yardımıyla farklı sürelerde hesaplatmaya çalışalım. Burada seçilen formülün Standat Normal Dağılım olmasının hiç bir önemi yoktur. İsterseniz x=2y formülünü de kullanabilirsiniz. Ben işlemler uzun sürmesi açısından bu formülü seçtim.

Formülümüz şu şekildedir:

Bu formülü for döngüsünü 10, 100, 1.000, 10.000, 30.000, … şeklinde farklı değerlere kadar seri ve paralel şeklide hesaplattırıp sonucu bir tabloda incelemeye çalışalım.

İşlemi öncelikler seri işlemler mantığı ile yaparak sonuca bakacak olursak, bir işlemin bitmeden diğerinin başlamadığını göreceğiz. Döngümüz işlemi 10000’e kadar 30 kez yapacak.

For döngüsü adımsal işlem gerçekleştirmekte ve  her döngüyü aynı thread gerçekleştirmektedir. Tüm işlemler 1 numaralı thread tarafından gerçekleştirilmektedir.

Şimdi de işlemi paralel olarak gerçekleştirelim. Aynı şeklide işlemi 10000’e kadar 30 kez yapacak.

Paralel işlemlerde kullandığımız tip System.Threading.Tasks.Parallel olacaktır. Paralel for kullanımı şeklideki gibi basitçe ifade edilmektedir ancak arka planda Task tipleri hemen görevleri bölüşürler.

Paralel işlemi incelediğimizde işlemin 4 farklı thread tarafından yapıldığını ve sürenin seri işlemlerden daha uzun sürdüğünü görmekteyiz. Ancak döngü sayısını arttırdığımızda şu şeklide bir tablo karşımıza çıkacaktır.

Döngü sayısı arttıkça paralel işlemlerin adımsal işlemlerden daha kısa sürede gerçekleştiğini görmekteyiz. Örneğimizde kritik sınır 30.000 olarak görünmektedir.

Senaryonun grafiksel ifadesinden de durumu bu şeklide inceleyebiliriz. Bu sistemi test ederken kullandığım laptop biraz eski olduğundan işlemler uzun sürdü. Intel Centrino Core 2 işlemci üzerinde sonuçları bu şeklide aldım. Farklı işlemcilerde sonuçları test edebilirsiniz. Yorumlara yazabilirseniz karşılaştırma yapabiliriz. Ancak benim deneme yaptığım işlemci modelinden kimsede kalmadığından emin gibiyim. Nesli tükendi artık.

Bir sonraki yazıda görüşmek dileğiyle.

C# ile Threading işlemleri

17 Oca

Threading

Threading konusu, yazılım geliştirme sürecinde çok önemli bir kavramdır. Thread’ler sayesinde birçok işi aynı anda eş zamanlı olarak yapmak mümkündür. Yürütülen iş parçacıklarını bir süre bekletmek  veya istenen anda sonlandırmakta mümkündür.

Threading işlemlerini yöneten tipler, .Net Framework tarafında System.Threading alanında barınmaktadır.

Threading konusuna başlamadan önce Thread sınıfını tanımak gerekmektedir. Bu sınıf tek bir iş parçacığı nesnesidir. Yani bir iş parçacığını başlatmayı ve süreci yönetmeyi sağlar.

Bir iş parçacığının oluşturulması şu şekilde olmaktadır.


class SingleThreading
{
  public void StartSingleOperation()
  {
    Thread thread = new Thread(new ThreadStart(work1));
    thread.Start();
  }

  private void work1()
  {
     Console.WriteLine("This is worker thread. ThreadID: {0}",
     Thread.CurrentThread.ManagedThreadId);
  }

}

Burada SingleThreading sınıfı içersinde work1 isimli işi yapan, thread nesnesidir. Yani StartSingleOperation metodu çalıştığında programdaki diğer yürütülen işlemlerin bitmesini beklemeden work1 isimli iş yapılmaya başlanır.

Multiple Thread Kullanımı

Bazı durumlarda birden fazla thread kullanarak iş süreçlerini yönetmek istediğimiz durumlar olabilir. Yani birbirini etkilemeyen, sıra beklemeden eş zamanlı olarak yapılması planlanan süreçleri için aşağıdaki örneği verebiliriz.


class MultipleThreading
{
  public void StartMultipleWriter()
  {

     Thread th1 = new Thread(new ThreadStart(WriteX));
     Thread th2 = new Thread(new ThreadStart(WriteO));

     th1.Start();
     th2.Start();
  }

  private void WriteX()
  {
    for (int i = 0; i < 300; i++)
    {
       Console.Write("X");
    }
  }

  private void WriteO()
  {
     for (int i = 0; i < 300; i++)
     {
       Console.Write("O");
     }
  }

}

Burada th1 ve th1 isimli iki nesne örneği birbirinden bağımsız olarak iki ayrı iş parçacığını yönetmektedir. Burada işlemci iş yoğunluğuna göre eşzamanlama işlemini gerçekleştirmektedir. Yani iki farklı iş bitene kadar biraz WriteX metodundan birazda WriteO metodundan yürütme işlemi yapmaktadır. Bunun ispatı olarak da şu ekran çıktısını verebiliriz.

Görüldüğü gibi bir süre X yazma işlemi yapılmış, bir süre de O yazma işlemi yapılmış. Program her çalıştığında X ve O farklı yerlerde olabilir. Çünkü işlemcinin durumuna göre hangi metodun ne kadar çalışacağı belirlenmektedir.

Önceliklerin belirlenmesi (Thread Priority)

Bazı durumlarda çoklu thread kullanırken öncelik vermek istenen iş parçacıkları olabilir. Birçok thread aynı anda start edildiğinde, bunlardan bir tanesi diğerlerine göre öncelikli olması gerekebilir. Bu gibi durumlarda imdadımıza yetişen Thread.Priority özelliğidir. Bu özellik bir enumerasyon tipindedir. Bu enum tipi, bünyesinde; Highest, AboveNormal, Normal, BelowNormal, Lovest gibi seçenekleri tutmaktadır.

Yukardaki örnekte iş parçacıkları başlatılmadan önce öncelikleri şu kelikde belirleyelim.

public void StartMultipleWriter()
{
   Thread th1 = new Thread(new ThreadStart(WriteX));
   Thread th2 = new Thread(new ThreadStart(WriteO));

   th1.Priority = ThreadPriority.Lowest;
   th2.Priority = ThreadPriority.Highest;

   th1.Start();
   th2.Start();

}

Yani WriteX metodu WriteO metodundan daha az öncelikli olsun. Bu durumda işlemci, WriteO metodunun işlemini daha önce bitirecektir.

Ekran çıktısından da anlaşılacağı gibi WriteX metodu daha az öncelikli olduğundan X yazma işlemi sonlara bırakılmıştır.

Thread.Join metodu ile iş parçacığının bitmesini beklemek

Thread.Join() metodu bir iş parçacığının başka bir göreve başlamadan önce tamamlanması için bekletir. Ayrıca metodun overload edilmiş bir hali ile, belirlenen bir süre kadar işlemin yapılması sağlanır. Belirlenen süre içinde işlem biterse metod geriye True değer döndürür, aksi halde False döndürür.

Örneğin “Thread1” ve “Thread2” adında iki iş parçacığını sırayla çalıştırdığımızı varsayalım. “Thread1” çalışırken ”Thread2.Join()” metodunu çağırdığımızda “Thread2” işlemini bitirene kadar “Thread1” bekler. Ardından “Thread1” işlemine devam eder. Eğer ”Thread2.Join(int)” metodu çağırıldğında ise, belirlenen süre içinde işlem bitmezsse bekleyen diğer iş parçacığı çalışmaya başlar.

Bu yazımızda threading işlemlerinden  bahsetmiş olduk. Umarım faydalı bir yazı olmuştur.