İşletim sistemleri, verilen görevleri iş parçacıkları ile süreçler halinde yerine getirirler. İşletim sistemindeki bir süreç, temel olarak yürütülmekte olan bir programdır. Program içerisinde yürütülmesi istenen işler bir veya birden fazla thread (iş parçacığı) ile yürütülebilmektedir. Bilgisayar programlamada dilin sağladığı özellikler sayesinde iş parçacıkları asenkron olarak yönetilebilmektedir. Her program bir ana thread ile ayağa kalkıp işi bitene kadar bu thread ile yürütülmektedir. Programı asenkron yönetmek, programın alt süreçlerini, ana thread dışında bir veya birden fazla thread ile yürütmektir.
Özellikle çok çekirdekli işlemcilerden tam performans alabilmek için birden fazla thread ile asenkron işlemler gerçekleştirilebilmektedir. Buna multithreading denilmektedir.
C# Thread ve Task sınıfları
C# programlama dilinde asenkron işlemler için Thread ve Task olmak üzere iki sınıf tanımlanmıştır. (Linkler .Net 7.0 versiyonu doküman sayfasına aittir.)
Thread Sınıfı
Thread sınıfı bir iş parçacığı oluşturup denetler, çalışma önceliğini ayarlar ve durumunu bildirir. Geriye bir değer döndürmeyen, asenkron çalışan tek bir işlemi temsil eder. C# ortamında çalışan bir thread hakkında bazı temel bilgileri elde etmek mümkündür.
// Thread Id bilgisi
Thread.CurrentThread.ManagedThreadId
// Thread durum bilgisi
Thread.CurrentThread.ThreadState
// Background olup olmadığı bilgisi
Thread.CurrentThread.IsBackground
//ThreadPool'dan alınıp alınmadığı bilgisi
Thread.CurrentThread.IsThreadPoolThread
Thread nesnesi oluşuturma örneği;
// Run with new Thread
var thread = new Thread(()=>Write("*"));
thread.Start();
// Run with Main Thread
Write("-");
void Write(string input)
{
for (var i = 0; i < 10; i++)
Console.Write(input);
}
// Output
--**----********----
Aynı Write metodu hem main thread hemde thread nesnesi tarafından çalıştırılmaktadır. Output incelendiğinde biraz “*” karakteri biraz “-” karakterinin ekrana yazıldığı görülmektedir.
C# dilinde yönetilen (managed) thread,ya arka planda (background) ya da ön planda (foreground) çalışır. Thread’in bu özelliğini öğrenmek için Thread.IsBackground property kullanılır.
Foreground Thread
Foreground thread, main thread’in işini bitirip sonlanmasından etkilenmez. Kendi işi bittiğinde sonlanır. Bu, C#’ta ön plandaki bir iş parçacığının ömrünün ana iş parçacığına bağlı olmadığı anlamına gelir. Thread’ler default olarak foreground özelliğinde oluşturulurlar.
var thread = new Thread(()=>Write("*"));
thread.Start();
Console.WriteLine("Main thread finished");
void Write(string input)
{
for (var i = 0; i < 10; i++)
{
Thread.Sleep(100);
Console.Write(input);
}
}
// Output
Main thread finished
**********
thread.Start() metodu çalıştırıldıktan sonra main thread Console.WriteLine(“Main thread finished”); metodunu çağırır ve main program sonlanır. Ancak thread nesnesi, foreground özelliğinde olduğu için işini bitirene kadar çalışmaya devam eder.
Background Thread
Background thread’lerin çalışması main thread veya diğer tüm foreground thread’lerin çalışır olması durumda olmasına bağlıdır.
var thread1 = new Thread(()=>Write("*"));
thread1.IsBackground = true;
thread1.Start();
Console.WriteLine("Main thread finished");
void Write(string input)
{
for (var i = 0; i < 10; i++)
{
Thread.Sleep(100);
Console.Write(input);
}
}
// Output
Main thread finished
Burada main thread Console.WriteLine(“Main thread finished”); metodunu çağırıp hemen sonlandığı için background thread de işini yapamadan yani ekrana birşey yazamadan sonlanmıştır. Eğer bir foreground thread2 tanımlanıp çalıştırılsaydı, o bitene kadar thread1 işini yapacaktı.
Task Sınıfı
Task sınıfı asenkron bir operasyonu temsil eder. Task-based asyncronious pattern kavramının tamel yapı taşıdır. İşlem sonucunda geriye bir değer döndürebilme özelliğine sahiptir.
Lambda ifadesi ile task nesnesi oluşturma;
Task t1 = new Task(
() => Console.WriteLine("Working"));
t1.Start();
// t1'nin çalıştığını göstermek için main thread bloke edilsin.
t1.Wait();
Parametre alan Lambda ifadesi ile task nesnesi oluşturma;
Task t1 = new Task(
param=> Console.WriteLine($"Working param: {param}"), 2);
t1.Start();
// t1'nin çalıştığını göstermek için main thread bloke edilsin.
t1.Wait();
Action metod task nesnesi oluşturma;
var action = (object? param) =>{
Console.WriteLine($"I am working with param: {param}");
};
Task t1 = new Task(action, 2);
t1.Start();
// t1'nin çalıştığını göstermek için main thread bloke edilsin.
t1.Wait();
Task nesneleri arka plan (background) thread olarak hizmet vermektedir. Bu nedenle main thread sonlanmadan çalışmasını görebilmek için t1.Wait(); kullanılmıştır.
var action = (object? param) =>{
Console.WriteLine(Thread.CurrentThread.IsBackground);
Console.WriteLine(Thread.CurrentThread.IsThreadPoolThread);
Console.WriteLine($"I am working with param: {param}");
};
Task t1 = new Task(action, 2);
t1.Start();
t1.Wait();
Task nesnelerinin kullandıkları thread, ThreadPool’a aittir. Bunu Thread.CurrentThread.IsThreadPoolThread property ile anlayabiliriz.
ThreadPool Nedir? ThreadPool Neden Kullanılır?
ThreadPool tipi, uygulamada görevleri yürütmek, asenkron I/O süreçlerini işletmek, timer’ları işletmek gibi işlemler için bir thread havuzu sunar. Task objeleri, bu havuz içerisinden bir thread alarak işlemlerini yürütürler. Bu sayede thread oluşturma, oluşturulan thread için resource ayırma, görevi yürütme ve Garbage Colleciton ile kaynakların serbest bırakılması adımları tekrar tekrar işletilmez. ThreadPool’da arka planda bir dizi görevi gerçekleştirmek için yeniden kullanılabilecek bir thread havuzundan yeniden kullanılabilir thread’lerden bir tane alınarak işlemeler gerçekleştirilir ve thread havuza geri bırakılır.
ThreadPool üzerindeki aktif thread sayısını şu şekilde öğrenmek mümkündür;
Console.WriteLine(ThreadPool.ThreadCount);
ThreadPool üzerinde tanımlanabilecek maksimum worker thread sayısı ve maksimum I/O işlemleri için kullanılabilecek thread sayısını şu şekilde öğrenilebilir;
ThreadPool.GetMaxThreads(out var worker, out var ioCompletion);
Console.WriteLine("{0} / {1}", worker, ioCompletion);
Task içerisinde çalışan thread, ThreadPool içerisinden alınır.
Task Oluşturma ve Yürütme
Bir taskı oluşturmak ve yürütmek için birden fazla yöntem vardır. En yaygın kullanılanı ise Task.Run() metodudur. Bu yöntemde Task default parametreleri ile oluşturulur ve kullanılır.
Task.Run(() =>
{
Console.WriteLine("Working");
Console.WriteLine(Thread.CurrentThread.IsBackground);
Console.WriteLine(Thread.CurrentThread.IsThreadPoolThread);
});
Bir diğer yöntem ise Task.Factory.StartNew() statik metodunun çalıştırılmasıdır. Bu yöntemde StartNew() metodunun overload edilmiş tipleri ile Task’a parametre geçilebilmektedir.
Task.Factory.StartNew(() =>
{
Console.WriteLine("Working");
Console.WriteLine(Thread.CurrentThread.IsBackground);
Console.WriteLine(Thread.CurrentThread.IsThreadPoolThread);
});