Senkron & Asenkron ve MultiThread Programlama -6 — ThreadPool & WaitHandles

Alperen Öz
7 min readJul 7, 2024

--

ThreadPool & WaitHandles

Photo by Austin Distel on Unsplash

Thread Pool Nedir?

  • C# dilinde çoklu iş parçacığı (multi-threading) programlama yaklaşımında kullanılan Thread Pool mekanizması, birçok iş parçacığını yönetmek ve yeniden kullanılabilir hale getirmek için tasarlanmış bir yapıdır.
  • C# dilinde kullanılan Thread Pool mekanizması, çok sayıda iş parçacığını efektif bir şekilde yönetip tekrar kullanılabilir hale getirmek için tasarlanmıştır.
  • Bir iş parçacığı oluşturduğunuzda, yaklaşık 1 MB bellek kullanımı gerektirebilir ve yeni bir iş parçacığının başlatılması işlemci maliyetine yol açabilir.
  • Thread Pool, çok sayıda iş parçacığının yönetim yükünü azaltır ve CPU çekirdeklerinin daha verimli kullanılmasını sağlayarak, işlemci kaynaklarını daha etkin bir şekilde kullanmaya odaklanır.
  • Bu mekanizma, gereksiz iş parçacığı oluşturmayı önleyerek ve iş parçacıklarının ek yüklerini minimize ederek, çoklu iş parçacıklı işlemlerin daha düşük düzeyde performansla gerçekleştirilmesini sağlar.

Thread Pool Nasıl Bir Yapıya Sahiptir?

  • Bir Thread Pool, önceden oluşturulmuş bir grup iş parçacığı içerir. Bu iş parçacıkları belirli işlemleri gerçekleştirmek için havuzda beklerler. Bir iş tamamlandığında, havuza geri dönerler ve yeni bir iş için hazır olurlar.
  • Bu sayede, iş parçacıklarının oluşturulması ve sonlandırılması maliyeti yazılımdan topluca kaldırılmış olur.

Thread Pool Kullanımı Nasıldır?

ThreadPool.QueueUserWorkItem(WorkerMethod, "Task 1");
ThreadPool.QueueUserWorkItem(WorkerMethod, "Task 2");
ThreadPool.QueueUserWorkItem(WorkerMethod, "Task 3");
ThreadPool.QueueUserWorkItem(WorkerMethod, "Task 4");
ThreadPool.QueueUserWorkItem(WorkerMethod, "Task 5");
ThreadPool.QueueUserWorkItem(WorkerMethod, "Task 6");
Console.ReadLine();
void WorkerMethod(object state)
{
Console.WriteLine($"*** Thread Count: {ThreadPool.ThreadCount}");
string jobName = (string)state;
Console.WriteLine($"Task Started: {jobName}");
Thread.Sleep(new Random().Next(1000, 5000));
Console.WriteLine($"Task Done: {jobName}");
}
//*** Thread Count: 6
//Task Started: Task 2
//*** Thread Count: 6
//Task Started: Task 1
//*** Thread Count: 7
//Task Started: Task 5
//*** Thread Count: 6
//Task Started: Task 3
//*** Thread Count: 8
//Task Started: Task 6
//*** Thread Count: 6
//Task Started: Task 4
//Task Done: Task 3
//Task Done: Task 1
//Task Done: Task 5
//Task Done: Task 6
//Task Done: Task 4
//Task Done: Task 2
  • ThreadPool.QueueUserWorkItem metodu kullanılarak C#’ta Thread Pool’dan iş parçacığı alınır ve WorkerMethod çalıştırılır.
  • Görüldüğü üzere, program çalışmaya başlamış, 6 kez WorkerMethod çağırıldığı ve threadPool üzerinden bu işlem yapıldığı gözlemleniyor.
  • Çıktıya bakıldığında, Threadpool’un metot çağrılarına ilk Task 2 parametreli çağrıya thread atadığını daha sonra diğerleri ile devam ettiğini gözlemliyoruz. Bu sıralama tabi ki değişebilir.
  • Ayrıca her çağrı sırasında threadPool’daki faal halde olan thread sayısını da gözlemliyoruz.

Bu faal thread sayısını threadPool kendi iradesinde arttırabilir veya azaltabilir.

Thread Pool Nasıl Optimize Edilir?

  • Thread Pool, ilk oluşturulduğunda tek bir iş parçacığı ile başlar. Yeni görevler geldikçe havuz genişler, yeni iş parçacıkları oluşturulur ve gerektiğinde mevcut iş parçacıkları devreden çıkarılır.
  • Ayrıca ThreadPool optimizasyon amaçlı içerisinde barındırdığı thread’leri background’da çalışacak şekilde yapılandırır. Böylece uygulama durdurulduğunda bütün thread’ler tırotomatik olarak durdurulmuş olacaktır.

ThreadPool’a atılmış bir thread’i iptal edebilmenin yolu yoktur.

  • ThreadPool’a optimize amaçlı yapabileceğimiz dokunuş sadece eş zamanlı çalışacak thread sayısının maksimum ve minimumunu belirlemektir.
  • SetMaxThreads metodunu kullanarak havuzdaki eşzamanlı çalışacak iş parçacığı sayısını, benzer şekilde SetMinThreads metoduyla minimum iş parçacığı sayısını belirleyebilirsiniz.
  • SetMinThreads metodunun işlevi ilk başta anlamsız gelebilir fakat optimizasyon açısından kritiktir.
  • Thread Pool’da her iş parçacığının oluşturulması ve işlevsel bilgiler ışığında sistem kaynakları açısından bir maliyeti olacaktır. Bu maliyet, iş parçacığı oluştururken karşılaşılan maliyetlerle benzerdir. Bu nedenle, bu maliyetin Thread Pool’da minimumda tutulması hedeflenir.
  • Bu maliyetlerden kaçınmak ve belirli sayıda iş parçacığının her zaman aktif olmasını sağlamak için SetMinThreads metodunu kullanabilirsiniz. Böylece belirli bir sayıda iş parçacığının her zaman hazır olmasını ve iş yükünü karşılamasını sağlayabilirsiniz.
ThreadPool.SetMaxThreads(4,4);
ThreadPool.SetMinThreads(2,2);

ThreadPool.QueueUserWorkItem(WorkerMethod, "Task 1");
ThreadPool.QueueUserWorkItem(WorkerMethod, "Task 2");
ThreadPool.QueueUserWorkItem(WorkerMethod, "Task 3");
ThreadPool.QueueUserWorkItem(WorkerMethod, "Task 4");
ThreadPool.QueueUserWorkItem(WorkerMethod, "Task 5");
ThreadPool.QueueUserWorkItem(WorkerMethod, "Task 6");
Console.ReadLine();
void WorkerMethod(object state)
{
Console.WriteLine($"*** Thread Count: {ThreadPool.ThreadCount}");
string jobName = (string)state;
Console.WriteLine($"Task Started: {jobName}");
Thread.Sleep(new Random().Next(1000, 5000));
Console.WriteLine($"Task Done: {jobName}");
}
//*** Thread Count: 6
//Task Started: Task 2
//*** Thread Count: 6
//Task Started: Task 1
//*** Thread Count: 7
//Task Started: Task 5
//***Thread Count: 6
//Task Started: Task 3
//***Thread Count: 8
//Task Started: Task 6
//***Thread Count: 6
//Task Started: Task 4
//Task Done: Task 1
//Task Done: Task 2
//Task Done: Task 3
//Task Done: Task 5
//Task Done: Task 6
//Task Done: Task 4
  • Yukarıdaki örnekte SetMinThreads ve SetMaxThreads metotları kullanılarak ThreadPool’daki oluşması istenen minimum ve maksimum thread sayısı belirlenmiştir.
  • Fakat çıktı da Görüldüğü üzere bu koyduğumuz Min ve Max değerlere ThreadPool’un dikkat etmediğini ve limit dışı değerler set ettiğini gözlemliyoruz.

Max ve min değerler belirlenmesine rağmen ThreadPool bu max min değerlere uyacağının garantisini vermez. Bu değerler ThreadPool için tavsiye niteliğindedir. Nihai kararı yine ThreadPool kendisi verir.

  • SetMaxThreads ve SetMinThreads metodlarının ikinci parametreleri olan completionPortThreads parametresiyle, bir thread çalışırken başka thread’in I/O işlemlerinin asenkronik çalışması sağlanmaktadır. Belirtilen miktar kadar thread’in I/O işlemi tamamlanana kadar bloke olması önlenir ve böylece Thread Pool tarafından kaynakların daha optimize bir şekilde kullanılması sağlanabilir.

Thread Pool’un hangi senaryolarda kullanılması önerilir?

  • Yoğun iş yükü olan uygulamalar: Thread Pool, yoğun iş yükü altındaki uygulamalarda kullanılarak işlemlerin daha verimli bir şekilde yönetilmesini sağlar. Özellikle çok sayıda iş parçacığının eş zamanlı olarak çalıştırılması gereken durumlarda tercih edilir.
  • Ağ tabanlı uygulamalar: Ağ ile iletişim kurma gereksinimi olan uygulamalar, Thread Pool yapısını kullanarak ağ isteklerini paralel olarak işleyebilir. Bu sayede ağ ile ilgili gecikmeleri minimize edebilir ve daha iyi performans elde edebilirler.
  • Web sunucuları ve hizmetleri: Web sunucuları ve hizmetleri, gelen istekleri paralel olarak işleyerek daha hızlı yanıt vermek için Thread Pool yapısını kullanabilirler. Bu sayede aynı anda çok sayıda isteği karşılayabilirler.
  • GUI uygulamaları: Grafik arayüzlü uygulamalar, kullanıcı etkileşimlerini ve işlemleri paralel olarak yönetmek için Thread Pool yapısını kullanabilir. Bu, kullanıcı deneyimini artırabilir ve uygulamanın daha akıcı çalışmasını sağlayabilir.
  • Veritabanı işlemleri: Veritabanı işlemleri genellikle gecikmeli işlemlerdir ve Thread Pool yapısı kullanılarak bu işlemler paralel olarak yönetilebilir. Bu, uygulamanın daha hızlı ve verimli olmasını sağlayabilir.

Thread Pool yapısının bu senaryolarda kullanılması, iş parçacıklarının daha verimli bir şekilde yönetilmesini ve sistem performansının artırılmasını sağlar.

Threadpool ne amaçla kullanılmaktadır?

  • Performans artışı sağlama: Thread Pool, gereksiz Thread oluşturulmasını önler ve Thread yönetimini optimize ederek performansı artırır. Bu sayede işlemci kaynakları daha verimli kullanılır ve uygulamanın genel performansı artar.
  • Ölçeklenebilirlik desteği: Thread Pool, çok sayıda Thread, yönetebilme ve sistem performansını etkin bir şekilde dengeleme yeteneği sağlar. Bu özellik, uygulamanın ölçeklenebilir olmasını ve farklı yük seviyelerine uyum sağlamasını sağlar.
  • Kaynak yönetimi optimizasyonu: Thread Pool, threadlerin tekrar kullanımını sağlayarak kaynakların etkili bir şekilde yönetilmesini sağlar. Bu da sistem stabilitesini artırır ve kaynak israfını önler.
  • Yük dengeleme ve sistemi stabilize etme: Thread Pool, iş yükünü eşit şekilde dağıtarak sistemde dengeli bir performans elde edilmesini sağlar. Aynı zamanda bekleyen işlemleri sıraya alarak sistemdeki yoğunluğu dengeleyerek uygulamanın genel stabilitesini artırır.
  • Uygulama Duyarlılığı: ThreadPool, threadleri yeniden kullanarak gereksiz thread oluşturma ve sonlandırma maliyetini azaltmakta, bu sayede uygulamanın daha hızlı ve daha duyarlı olmasını sağlamaktadır. Özellikle kullanıcı etkileşimlerinde hızlı yanıt gerektiren uygulamalarda bu çok kritik bir faktördür.

ThreadPool’un Dezavantajları:

  • Sınırlı Kontrol: ThreadPool’da thread’ler üzerinde çok fazla kontrol sahibi değilsiniz. Thread’lerin önceliğini veya çalıştırılma zamanını belirleyemiyorsunuz.

Task Sınıfı ve ThreadPool

  • Task sınıfı, asenkron işlemleri temsil eder ve çalıştırır. Task sınıfı, işlemleri gerçekleştirmek için arka planda ThreadPool’dan iş parçacıklarını alır.
  • Task.Run, Task.Factory.StartNew gibi metodlar kullanıldığında, Task nesnesi oluşturulur ve bu nesne arka planda bir ThreadPool iş parçacığı üzerinde çalıştırılır.
  • Task sınıfı, .NET’in sağladığı TPL (Task Parallel Library) ile birlikte çalışarak, paralel programlama ve iş parçacığı yönetimini daha kolay ve esnek hale getirir.
  • Task’ların ThreadPool ile entegrasyonu, kaynakların daha verimli kullanılmasını sağlar ve sistem performansını artırır.

Thread Sınıfı ve ThreadPool

  • Thread sınıfının, doğrudan iş parçacıkları oluşturmak ve yönetmek üzerine kurgulanmış bir yapı oldugunu söylemiştik.
  • Thread sınıfı, ThreadPool’dan bağımsız olarak çalışır ve her yeni iş parçacığı oluşturma işlemi, yeni bir sistem iş parçacığı yaratır.
  • Bu yöntem daha fazla esneklik ve kontrol sağlar, ancak daha fazla kaynak tüketir ve yönetimi daha zordur.

Özet

ThreadPool, C# ve .NET uygulamalarında iş parçacıklarını etkin bir şekilde yönetmek için kullanılan güçlü bir araçtır. İş parçacıklarının oluşturulması ve yok edilmesi maliyetini düşürerek, performansı artırır ve kaynak kullanımını optimize eder. ThreadPool, kısa ömürlü ve sık sık yapılan işlemler için idealdir ve Task sınıfı ile birlikte kullanıldığında, asenkron programlama ve paralel işlemleri daha kolay ve verimli hale getirir.

Wait Handles Nedir?

  • Wait Handles; bir thread’in faal olan diğer bir thread’in operasyonlarının tamamlanmasını beklememizi sağlayan signalling yaklaşımını uygulayan bir mekanizmadır.
  • Esasında, bir öncek signalling makalesinde bahsedilen AutoResetEvent, ManuelResetEvent birer wait handles yapılanmalarıdır. Yani bunlar aslındawait handles mekanizmasını ta kendisidir.

Wait Handles’ların ThreadPool ile Kullanımı

  • Wait Handles ve ThreadPool.RegisterWaitForSingleObject metodu, ThreadPool içindeki iş parçacıklarının senkronizasyonunu ve olay temelli işlemlerini etkin bir şekilde yönetmek için kullanılır, böylece uygulama performansı ve verimliliği artırılır.
  • ThreadPool.RegisterWaitForSingleObject metodu, özellikle ThreadPool içinde çalışan iş parçacıklarının, belirli bir WaitHandle (bekleme işareti) nesnesinin sinyal vermesini beklemesi için kullanılır. Bu yöntem, asenkron operasyonlar sona erdiğinde bildirim almayı sağlar ve bu işlemleri bloklamadan tamamlamak için idealdir.
AutoResetEvent autoResetEvent = new(false);
RegisteredWaitHandle registeredWaitHandle = ThreadPool.RegisterWaitForSingleObject(autoResetEvent, WorkerMethod, "Task 1 Wait Handle", -1, true); //No Time Out!
//RegisteredWaitHandle registeredWaitHandle = ThreadPool.RegisterWaitForSingleObject(autoResetEvent, WorkerMethod, "Task 1 Wait Handle", 15000, true); //will active autmaticly after 1.5 seconds...!
Thread.Sleep(2500);
autoResetEvent.Set();
registeredWaitHandle.Unregister(autoResetEvent);
Console.Read();
void WorkerMethod(object state, bool timedOut)
{
Console.WriteLine($"*** Thread Count : {ThreadPool.ThreadCount}");
string name = (string)state;
Console.WriteLine($"{name} started");
Thread.Sleep(new Random().Next(1000, 5000));
Console.WriteLine($"{name} done");
}
//*** Thread Count : 5
//Task 1 Wait Handle started
//Task 1 Wait Handle done
  • parametreye -1 değeri verilerek zaman aşımı süresi tanımlanmamakta ve sinyal gelene kadar beklenileceği ifade edilmektedir.
  • Eğer burada bir değer girilmiş olsaydı zaman aşımı süresi tamamlandığı taktirde ilgili thread otomatik olarak çalıştırılacaktı.
  • Ayrıca 5. parametredeki true değeri ise ilgili thread’in sadece bir kere çalıştırılacağını ifade etmekte, eğer false verilseydi her zaman aşımı gerçekleştiğinde de tekrar tekrar çalıştırılabilecektir.

Bu metot ile işlem bittiği taktirde sistem kaynaklarını serbest bırakmak için Unregister metodunu kullanmayı unutmamak lazımdır! registeredWaitHandle1. Unregister(autoResetEvent);

Signalling — Yardımcı Metotlar

Signalling davranışının sergilendiği çalışmalarda birden fazla farklı türde signal’ın durumlarını kontrol etmek için bu metotlardan istifade edebilmekteyiz.

WaitHandle.WaitAll

Dizi olarak almış olduğu tüm signal’lardan bir sinyal alana kadar kullanıldığı thread’i bekletecek ta ki tüm sinyalleri aldığı taktirde bu metodu çağıran thread’in çalışması devam edecektir.

Sinyaller arasında and (&) koşulu var gibi düşünülebilir.

AutoResetEvent autoResetEvent1 = new(false);
AutoResetEvent autoResetEvent2 = new(false);
ManualResetEvent manualResetEvent1 = new(false);
ManualResetEvent manualResetEvent2 = new(false);

autoResetEvent1.Set();
autoResetEvent2.Set();
manualResetEvent1.Set();
manualResetEvent2.Set();
WaitHandle.WaitAll(new WaitHandle[]
{
autoResetEvent1, autoResetEvent2 , manualResetEvent1 , manualResetEvent2
});
Console.WriteLine("Wait All");
Console.ReadLine();

WaitHandle.WaitAny

Dizi olarak almış olduğu signal’lar arasından herhangi birinden sinyal alana kadar kullanıldığı thread’i bekletecek ta ki bir sinyal geldiği taktirde bu metodu çağıran thread’in çalışması devam edecektir.

Sinyaller arası or (||) koşulu var gibi düşünülebilir.

AutoResetEvent autoResetEvent1 = new(false);
AutoResetEvent autoResetEvent2 = new(false);
ManualResetEvent manualResetEvent1 = new(false);
ManualResetEvent manualResetEvent2 = new(false);

manualResetEvent1.Set();
WaitHandle.WaitAny(new WaitHandle[]
{
autoResetEvent1, autoResetEvent2 , manualResetEvent1 , manualResetEvent2
});
Console.WriteLine("Wait Any");
Console.ReadLine();

WaitHandle.SignalAndWait

Bu metot iki adet signal almakta ve bu signallardan birincisine sinyal gönderildikten sonra diğeri için WaitOne metodu tetiklenerek diğerinin de sinyal vermesi beklenmektedir.

AutoResetEvent autoResetEvent1 = new(false);
AutoResetEvent autoResetEvent2 = new(false);

autoResetEvent1.Set();
autoResetEvent2.Set();

WaitHandle.SignalAndWait(autoResetEvent1, autoResetEvent2);
Console.WriteLine("Wait SignalAndWait");
Console.ReadLine();

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

--

--