System.Threading.Lock: .NET 9'da Thread Senkronizasyonu 🔒

System.Threading.Lock, .NET 9'da tanıtılan ve Multithreaded programlamada senkronizasyonu daha verimli ve güvenilir hale getirmeyi amaçlayan yeni bir sınıftır. Bu class, geleneksel Monitor class’a benzer şekilde çalışırken, bazı önemli avantajlar sunar.

Murat Dinç
Devops Türkiye☁️ 🐧 🐳 ☸️
6 min readJul 9, 2024

--

Merhabalar,

Daha önce yazmış olduğum yazılarda, thread’lerin sağlıklı yönetimi için distributed lock konularını işlemiştik. Race condition durumlarında bu tarz çözümlere giderken, .NET 9 ile hayatımıza giren System.Threading.Lock sınıfını inceleyeceğiz. Bu sınıf, özellikle Multithreaded uygulamalarda senkronizasyon sorunlarını çözmek için ideal bir araç olarak karşımıza çıkıyor. Tam olarak bu alanda imdadımıza yetişiyor desek yanlış olmaz 😊

Thread Yönetimi Nedir?

Thread yönetimi, bir yazılım uygulamasında birden fazla işin oluşturulması, senkronize edilmesi ve yönetilmesi sürecidir. Bu süreç, bir uygulamanın performansını ve verimliliğini artırmak amacıyla paralel işlemler gerçekleştirmek için kullanılır. Multithreaded programming, özellikle yoğun işlem gerektiren görevlerde veya aynı anda birden fazla işi gerçekleştirmek gerektiğinde büyük avantaj sağlar.

Thread yönetiminin temel unsurlarından biri, thread’lerin doğru ve verimli bir şekilde senkronize edilmesidir. Bu, aynı kaynağa erişen birden fazla thread’in birbirinin işini bozmasını engellemek için önemlidir (Race Condition). Geleneksel olarak Monitor ve lock gibi senkronizasyon mekanizmaları kullanılarak bu kontrol sağlanır.

Thread yönetimi, aynı zamanda thread’lerin oluşturulması ve yaşam döngülerinin yönetilmesini de içerir. Yeni bir thread oluşturmak, mevcut bir thread’in durdurulması veya yeniden başlatılması gibi işlemler, thread yönetiminin bir parçasıdır. Bu süreçlerin etkin bir şekilde yönetilmesi, uygulamanın genel performansını ve kullanıcı deneyimini doğrudan etkiler.

Race condition, multithreaded veya paralel programlama ortamlarında sıkça karşılaşılan ve ciddi sorunlara yol açabilen bir durumdur. Bu durum, iki veya daha fazla thread’in aynı kaynağa aynı anda erişmeye çalıştığı zaman ortaya çıkar ve bu erişim sırasında en az bir thread’in beklenmeyen veya hatalı sonuçlar üretmesine neden olur.

Concurrency ve Parallelism ile ilgilenen arkadaşlar daha önceden yazmış olduğum konu makalesini inceleyebilirler 👇🏻

System.Threading.Lock Nedir?

System.Threading.Lock, .NET 9'da tanıtılan ve Multithreaded programlamada senkronizasyonu daha verimli ve güvenilir hale getirmeyi amaçlayan yeni bir sınıftır. Bu class, geleneksel Monitor class'a benzer şekilde çalışırken, bazı önemli avantajlar sunar.

Methodlar

System.Threading.Lock sınıfı, thread senkronizasyonunu sağlamak için kullanılan birkaç temel metoda sahiptir. Bu metodlar, bir kaynağın yalnızca bir thread tarafından kullanılmasını garanti altına almak için kullanılır.

🔵 Enter Methodu

Enter() metodu, bir thread'in kilidi almasını sağlar. Eğer kilit başka bir thread tarafından alınmışsa, bu metodu çağıran thread kilidin serbest bırakılmasını bekler. Kilit serbest bırakıldığında, bekleyen thread kilidi alır ve kod bloğuna girebilir.

_lock.Enter();
try
{
// Kod bloğu
}
finally
{
_lock.Exit();
}

🔵 Exit Methodu

Exit() metodu, kilidi serbest bırakır. Bu metod, Enter() metoduyla alınan kilidin serbest bırakılmasını sağlar ve başka bir thread'in kilidi alabilmesi için yolu açar. Exit() metodunu çağırmak, Enter() metoduyla alınan her kilit için gereklidir.

_lock.Enter();
try
{
// Kod bloğu
}
finally
{
_lock.Exit();
}

🔵 TryEnter Methodu

TryEnter() metodu, kilidi hemen almayı dener ve kilit alınabilirse true, alınamazsa false döner. Bu metod, bir thread'in kilidi beklemek yerine hemen alıp alamayacağını kontrol etmesi gerektiğinde kullanışlıdır.

if (_lock.TryEnter())
{
try
{
// Kod bloğu
}
finally
{
_lock.Exit();
}
}
else
{
// Kilit alınamadı, alternatif işlem
}

🔵 TryEnter(TimeSpan timeout)

TryEnter(TimeSpan timeout) metodu, belirtilen süre boyunca kilidi almaya çalışır. Eğer kilit bu süre içinde alınabilirse true, aksi takdirde false döner. Bu metod, belirli bir süre bekledikten sonra kilidi alıp alamayacağını kontrol etmek için kullanılır.

if (_lock.TryEnter(TimeSpan.FromSeconds(5)))
{
try
{
// Kod bloğu
}
finally
{
_lock.Exit();
}
}
else
{
// Kilit alınamadı, alternatif işlem
}

🔵 TryEnter(int millisecondsTimeout)

TryEnter(int millisecondsTimeout) metodu, belirtilen milisaniye cinsinden süre boyunca kilidi almaya çalışır. Eğer kilit bu süre içinde alınabilirse true, aksi takdirde false döner. Bu metod, milisaniye bazında zaman aşımı belirlemek için kullanılır.

if (_lock.TryEnter(5000)) // 5 saniye
{
try
{
// Kod bloğu
}
finally
{
_lock.Exit();
}
}
else
{
// Kilit alınamadı, alternatif işlem
}

Avantajları

  1. Daha Yüksek Performans: Düşük seviyeli optimizasyonlar ve daha iyi kaynak yönetimi ile daha yüksek performans sağlar. Bu, özellikle çok sayıda thread'in aynı kaynağa erişmeye çalıştığı senaryolarda önemli bir avantajdır.
  2. Gelişmiş Ölçeklenebilirlik: Yeni mekanizma, büyük ölçekli uygulamalarda daha iyi ölçeklenebilirlik sunar. Bu sayede, sistem kaynakları daha verimli kullanılır ve uygulama performansı artar.
  3. Kolay Kullanım: Geliştiricilerin daha kolay ve anlaşılır bir şekilde senkronizasyon kodu yazmasına olanak tanır. Bu, kodun bakımını ve okunabilirliğini de artırır.

System.Threading.Lock Nasıl Kullanılır?

System.Threading.Lock sınıfını kullanmak için öncelikle bir Lock nesnesi oluştururuz. Ardından, kritik bölgeye erişmek istediğimizde Enter metodunu çağırarak kilidi elde ederiz ve işi bitirdikten sonra Exit metodunu çağırarak kilidi serbest bırakırız.

using System;
using System.Threading;

class Program
{
private static Lock _lock = new Lock();

static void Main(string[] args)
{
Thread thread1 = new Thread(new ThreadStart(ThreadMethod));
Thread thread2 = new Thread(new ThreadStart(ThreadMethod));

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

thread1.Join();
thread2.Join();
}

private static void ThreadMethod()
{
_lock.Enter();

try
{
Console.WriteLine("Thread ID: " + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(1000);
}
finally
{
_lock.Exit();
}
}
}

Bu örnekte, iki thread oluşturulmuş ve her biri ThreadMethod metodunu çalıştırmaktadır. ThreadMethod metodu içinde, System.Threading.Lock kullanılarak kritik bölgeye erişim sağlanmıştır. Bu sayede, aynı anda birden fazla thread'in kritik bölgeye erişmesi engellenmiştir.

Örnek: Banka Hesap Senaryosu 💵

Bir banka hesabımız var ve bu hesaptan hem para çıkışı hem de para girişi işlemi olabiliyor. Örneğin, hesabımız kartımıza bağlı ve bir yandan alışveriş yaparken, o anda tam da hesabımıza para girişi oldu. Bu iki işlem aynı anda aynı kaynağa erişmeye çalışıyor ve muhtemelen cüzdan bakiyesi hatalı olarak güncellenecektir. Önce, lock kullanımı olmadan hatalı senaryo üzerinde duralım, sonrasında ise lock kullanarak güvenli ve doğru işlemi yapacak olan kod örneğine bakalım.

class Program
{
static void Main(string[] args)
{
BankAccount account = new BankAccount();
Thread[] threads = new Thread[10];

// 5 thread para yatırıyor
for (int i = 0; i < 5; i++)
{
threads[i] = new Thread(() => account.Deposit(100));
}

// 5 thread para çekiyor
for (int i = 5; i < 10; i++)
{
threads[i] = new Thread(() => account.Withdraw(50));
}

foreach (Thread t in threads)
{
t.Start();
}

foreach (Thread t in threads)
{
t.Join();
}

Console.WriteLine("Final Balance: " + account.GetBalance());
}
}

class BankAccount
{
private decimal _balance = 0;

public void Deposit(decimal amount)
{
_balance += amount;
Console.WriteLine($"Deposited {amount}, New Balance: {_balance}");
}

public void Withdraw(decimal amount)
{
if (_balance >= amount)
{
_balance -= amount;
Console.WriteLine($"Withdraw {amount}, New Balance: {_balance}");
}
else
{
Console.WriteLine("Insufficient funds");
}
}

public decimal GetBalance()
{
return _balance;
}
}

Çıktı

Bu örnekteki hatalar, race condition’ın doğrudan bir sonucudur:

  1. Tutarsız Bakiyeler: Thread’ler aynı anda _balance değişkenine eriştiği için, bazı durumlarda bakiyeler yanlış hesaplanabilir. Örneğin, bir thread bakiyeyi okurken başka bir thread aynı anda bakiyeyi güncelleyebilir.
  2. Beklenmeyen Sonuçlar: Eşzamanlı erişim nedeniyle, işlemlerin sırası karışabilir ve beklenmeyen sonuçlar ortaya çıkabilir. Bu durum, son bakiyenin yanlış olmasına neden olur.

Şimdi ise olması gereken örneği yapalom.

using System.Threading;

class BankAccount
{
private Lock _lock = new Lock();
private decimal _balance = 0;

public void Deposit(decimal amount)
{
_lock.Enter();
try
{
_balance += amount;
Console.WriteLine($"Deposited {amount}, New Balance: {_balance}");
}
finally
{
_lock.Exit();
}
}

public void Withdraw(decimal amount)
{
_lock.Enter();
try
{
if (_balance >= amount)
{
_balance -= amount;
Console.WriteLine($"Withdrew {amount}, New Balance: {_balance}");
}
else
{
Console.WriteLine("Insufficient funds");
}
}
finally
{
_lock.Exit();
}
}

public decimal GetBalance()
{
return _balance;
}
}

class Program
{
static void Main(string[] args)
{
BankAccount account = new BankAccount();
Thread[] threads = new Thread[10];

// 5 thread para yatırıyor
for (int i = 0; i < 5; i++)
{
threads[i] = new Thread(() => account.Deposit(100));
}

// 5 thread para çekiyor
for (int i = 5; i < 10; i++)
{
threads[i] = new Thread(() => account.Withdraw(50));
}

foreach (Thread t in threads)
{
t.Start();
}

foreach (Thread t in threads)
{
t.Join();
}

Console.WriteLine($"Final Balance: {account.GetBalance()}");
}
}

Çıktı

Lock kullandığımız senaryoda işlemlerin sırası ile birbirlerini beklediklerini görüyoruz.

Makaleyi faydalı bulduysanız takip ederek destek olabilirsiniz 🙏

Bir sonraki yazıda görüşmek üzere 😊

--

--