Senkron & Asenkron ve MultiThread Programlama -2 — Thread Kavramı & Race Condition & Senkronizasyon Teknikleri

Alperen Öz
5 min readMay 1, 2024

Thread Kavramı & Race Condition & Senkronizasyon Teknikleri

Thread Sınıfı, yapılanması ve özellikleri

  • Thread’lerin bir işlem içinde bağımsız olarak çalışabilen en küçük yürütme birimi olduğunu, bir program çalışırken Öncelikle main thread olarak adlandırılan bir thread’in bulunduğunu, ayrıca bu thread’e ek olarak yardımcı thread’ler de oluşturabileceğimizi ve bunlara worker thread dediğimizi söylemiştik.
  • C#’da multithread yaklaşımını sergileyebilmek için Thread sınıfı başta olmak üzere System.Threading namespace’inden faydalanılır.
  • C#’da bir worker thread’in nasıl oluşturulacağı ve farklı overload’ları aşağıdaki örnekte verilmiştir.
class Program

private static void Main(string[] args) //main thread
{
Thread thread1 = new (()⇒ //worker thread oluşturma 1. yöntem
{ //ThreadStart overload'ı...

});

Thread thread2 = new ((o)⇒ //worker thread oluşturma.
{ // ParameterizedThreadStart overload'ı.

});

Thread thread3 = new (new ThreadStart(()=> //worker thread oluşturma 2. yöntem
{

}));


}
  • Bu şekilde Thread oluşturmak, oluşturduğumuz thread’in faal olduğu anlamına gelmez. Bu thread’i thread.Start() şeklinde çalıştırarak çalışır hale getirmiş oluruz.

Thread Id

  • C#’da oluşturulan her bir thread’e işletim sistemi tarafından bir Id verilir. Bu id sayesinde yapılan operasyonlar birbirinden ayırt edilebilir. Trace ve debug işlemlerinin takibi de bu Id üzerinden daha kolay gerçekleştirilebilir.
  • Ayrıca kaynak yönetimi ve Thread’ler arası haberleşmede de ThreadId’den faydalanılabilir.
System.Environment.CurrentManagedThreadId;            //Thread Id için 1. yöntem
Thread.CurrentThread.ManagedThreadId; //Thread Id için 2. yöntem

IsBackGround Field

  • IsBackGround field’i ile bir thread’in arka planda çalışıp çalışmayacağını belirleriz. Arka planda çalışacak olan yani IsBackGround property’si true olan bir thread, Main thread’e bağlı bir şekilde davranışını sürdürecektir. Yani main thread sona erdiğinde, bu arka plandaki thread de kendi işlemi bitsin veya bitmesin, otomatik sonlandırılacaktır.
  • IsBackGround property’si false olan bir thread ise main thread sonlansa bile çalışmaya devam edecektir. Bu threadi, main thread uygulamayı sonlandırabilmek için bitene kadar beklemek zorundadır.
  • Bu özellik arka planda çalışacak servisler veya uzun süren görevler gibi durumlarda uygulamanın davranışını kontrol edebilmek için kullanışlıdır.

Race Condition & Senkronizasyon Teknikleri

  • Her ne kadar Asenkron ve multithread programlama teknikleri faydalı ve gerekli olsa da bazen bu teknikler bazı problemlere yol açabilir.
  • Hızlı ve performanslı programlar geliştirmeye çalışırken, aynı zamanda bu programların Thread-Safe şekilde yani çoklu thread çalışmalarında thread’ler arası güvenli bir şekilde veri paylaşımı gerçekleştiriyor olmalıyız.

Yarış koşulu (race condition), çoklu iş parçacığı (multithread) ortamlarında karşılaşılan bir durumdur ve genellikle paylaşılan bir kaynağa aynı anda birden fazla iş parçacığının erişimine izin verildiğinde ortaya çıkan istenmeyen durumlardır.

  • Örneğin, bir değişkeni okuma, yazma veya değiştirme işlemi gerçekleştirirken, bir iş parçacığının bu işlemi yaparken diğer iş parçacıkları tarafından aynı değişken üzerinde benzer işlemlerin gerçekleştirilmesi durumunda yarış koşulları yani race condition oluşabilir. Bu durumda, hangi iş parçacığının önce işlemi tamamlayacağı belirsiz olabilir ve beklenmeyen sonuçlar veya hatalar ortaya çıkabilir.
  • Ya da bir thread henüz diğer thread’in tamamlanmamış işi üzerinde işlem yaparak da race condition’a sebebiyet verebilir.
  • Yarış koşulları, senkronizasyon teknikleri kullanılarak çözülür. Bu teknikler sayesinde birden çok iş parçacığı aynı anda bir kaynağı kontrollü bir şekilde erişebilecek ve kritik arz eden kaynaklarda sadece ilgili thread’in çalışmasını kontrol edebiliyor oluruz.
  • Yani özetle, bir kaynak birden fazla thread tarafından kullanılıyorsa, orada senkronizasyon mantığı ile hareket etmek gerekir.

C# ekosisteminin bizlere sunduğu birçok farklı senkronizasyon tekniği seçenekleri mevcuttur. Mevcut ihtiyaca en uygun senkronizasyon tekniği belirlenerek race condition durumu ortadan kaldırılır.

Senkronizasyon Teknikleri

Thread Locking

  • Locking mekanizması birden fazla iş parçacığının aynı anda paylaşılan kaynağa erişimini kontrol etmemizi sağlayan senkronizasyon tekniklerinden bir tanesidir.
  • Lock mekanizması aynı anda yalnızca bir iş parçacığının kritik bölgeye erişmesi sağlar ve yarış koşulları önlenmiş ve veri bütünlüğü sağlanmış olur.
  • Aşağıda önce lock mekanizması olmadan, bir de lock mekanizmasıyla race condition önlenmiş bir kod örneği ve çıktıları verilmiştir.

Lock mekanizmasının ve bunun gibi senkronizasyon tekniklerinin aşırı kullanımı performans sorunlarına yol açabilir, bu yüzden kritik bölgeyi mümkün olduğunca küçük tutmak önemlidir.

int i = 1;

Thread thread1 = new(() =>
{
while (i < 10)
{
i++;
Console.WriteLine($"Thread 1 : {i}");
}

});
Thread thread2 = new(() =>
{
while (i > 0)
{
i--;
Console.WriteLine($"Thread 2 : {i}");
}

});
thread1.Start();
thread2.Start();

//Output
//Thread 1: 1
//Thread 2: 1
//Thread 2: 1
//Thread 2: 0
//Thread 1: 2
//Thread 1: 1
//Thread 1: 2
//Thread 1: 3
//Thread 1: 4
//Thread 1: 5
//Thread 1: 6
//Thread 1: 7
//Thread 1: 8
//Thread 1: 9
//Thread 1: 10
//Thread 1: 11
  • Yukarıdaki örnek çıktıda görüldüğü üzere, yukarıdaki örnekte i kaynağı üzerinde işlem yapan thread1 ve thread2'de bir senkronizasyon kontrolü olmadığı için i nesnesinin değerinde tutarsızlık oluşmuştur.
object locking = new();          //referans nesne
int i = 1;

Thread thread1 = new(() =>
{
lock (locking)
{
while (i < 10)
{
i++;
Console.WriteLine($"Thread 1 : {i}");
}
}
});
Thread thread2 = new(() =>
{
lock (locking)
{
while (i > 0)
{
i--;
Console.WriteLine($"Thread 2 : {i}");
}
}
});
thread1.Start();
thread2.Start();

//Output

//Thread 1: 2
//Thread 1: 3
//Thread 1: 4
//Thread 1: 5
//Thread 1: 6
//Thread 1: 7
//Thread 1: 8
//Thread 1: 9
//Thread 1: 10
//Thread 2: 9
//Thread 2: 8
//Thread 2: 7
//Thread 2: 6
//Thread 2: 5
//Thread 2: 4
//Thread 2: 3
//Thread 2: 2
//Thread 2: 1
//Thread 2: 0
  • Yukarıdaki örnekte görüldüğü gibi, her iki thread’in erişebileceği bir referans nesne oluşturulmuş, koddaki kritik noktalara bu referans üstünden bir lock mekanizması kurulmuştur. Bu sayede ilgili i nesnesi üzerinde thread1 veya thread2’den önce işlemine başlayan thread işlemini bitirmiş, ardından diğer thread aynı nesne üzerindeki işlemini başlamış ve bitirmiştir. Aynı referans nesne üzerinden threadler işlem yapmak istediği kod bloğunun kilitli veya işlem yapılabilir olduğu bilgisine erişmiştir.
  • Kod çıktısından da görüldüğü üzere i nesnesinde herhangi bir veri tutarsızlığı olmadan farklı threadlerin çalıştırdığı kod blokları işlemini tamamlamıştır.

.NET9’dan itibaren object locking = new(); kod parçasındaki gibi object türünden değil direkt Lock sınıfı tipinde de bir instance oluşturabiliriz. Lock locking = new();

Thread Sleep

  • Thread.Sleep() yöntemi, bir iş parçacığının belirli bir süre boyunca uyumasını (beklemesini) sağlayan yöntemidir. Bu yöntem, belirtilen süre boyunca iş parçacığının işlem yapmasını duraklatır ve ilgili bekleme süresi dolduktan sonra thread tekrardan aktif hale gelir.
  • Genellikle, bir iş parçacığının belirli bir süre boyunca pasif olması gereken durumları işlemek için kullanılır. Örneğin, bir iş parçacığının belirli bir işi tamamladıktan sonra bir süre boyunca beklemesi gerekiyorsa veya bir döngü içinde bir aralık oluşturmak istiyorsanız Thread.Sleep() yöntemini kullanabilirsiniz.
Thread thread = new(() =>
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine(i);
Thread.Sleep(1000); //MS cinsinden parametre...
}
});
thread.Start();

Thread Join

  • Thread.Join() yöntemi, bir ana iş parçacığının, belirtilen diğer bir iş parçacığının tamamlanmasını beklemesi üzerine kurgulanmış bir senkronizasyon tekniğidir. Yani, ana iş parçacığı, Join() yöntemi çağrılan diğer iş parçacığının bitmesini bekler ve devam etmez.
  • Bu yöntem, çoklu iş parçacığı uygulamalarında farklı iş parçacıklarının senkronize edilmesi ve koordinasyonu için kullanılır.

Bu bekleme sürecinde main thread de bloklanacaktır.

Thread thread1 = new(() =>
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine($"Thread 1 {i}");
}
});
Thread thread2 = new(() =>
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine($"Thread 2 {i}");
}
});

thread1.Start();
thread1.Join();
thread2.Start();

//output

//Thread 1: 2
//Thread 1: 3
//Thread 1: 4
//Thread 1: 5
//Thread 1: 6
//Thread 1: 7
//Thread 1: 8
//Thread 1: 9
//Thread 2: 1
//Thread 2: 2
//Thread 2: 3
//Thread 2: 4
//Thread 2: 5
//Thread 2: 6
//Thread 2: 7
//Thread 2: 8
//Thread 2: 9
  • Yukarıdaki örnekte ve çıktıda görüldüğü gibi thread2 kendisinden önce join edilmiş thread’in işlemini bitirmesini beklemek durumundadır.

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

Sign up to discover human stories that deepen your understanding of the world.

Alperen Öz
Alperen Öz

Written by Alperen Öz

Software Engineer @SmartPulse

No responses yet

Write a response