Bir Concurrency Masalı: Senkronizasyon ve Kaynak Paylaşımı
https://jenkov.com/tutorials/java-concurrency/non-blocking-algorithms.html
Selam herkese. Bu yazıda birazcık senkronizasyon birazcık da kaynak paylaşımı konuşacağız.
Bir varmış bir yokmuş. Günlerden birgün Concurrency adında bir programlama konusu varmış. Bu konu öyle çetrefilliymiş ki alt dalları, dillere göre pattern’ları falan varmış.
Concurrency, gün geçtikçe yeni sorunlar ile gelirmiş. Mesela bunlardan bir tanesi senkronizasyon bir diğeri de kaynak paylaşımı imiş.
Peki Neymiş Bu Senkronizasyon?

Senkronizasyon, birden fazla thread’in ya da process’in birbirleriyle uyumlu bir şekilde çalışması için kullanılan bir tekniktir diyebiliriz.
Buraya kadar hafif tatlı bir tanımla senkronizasyona değindik diyebiliriz. Peki bu senkronizasyon neden vardır? Neyi önler? Bu soruyu sorduğunuzu duyar gibiyim.
Diyelimki elimizde birden fazla thread ya da process’in çalıştığı bir kod var. Bunların hepsi aynı resource’lar yani kaynaklar üzerinde işlem yapıyor olabilir. Bunlar memory üzerindeki bir bölge olabilir, bir dosyaya yazma olabilir ya da network üzerindeki bazı işlemler olabilir.
Bir kod örneği ile senkronizasyon sorununu anlamaya çalışalım;
class Program
{
private static int counter = 0;
static void Main(string[] args)
{
Thread t1 = new Thread(SayiyiBirerBirerArttir);
Thread t2 = new Thread(SayiyiBirerBirerArttir);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine($"Sayının güncel değeri: {counter}");
}
private static void SayiyiBirerBirerArttir()
{
for (int i = 0; i < 10000; i++)
{
counter++;
}
}
}
Baktığınızda bu kod bloğu çok normal bir kod bloğu ancak 2 farklı thread, aynı methodu çalıştırırken, bu method ise statik bir değişkenin değerini günceller. Her iki thread de 10 bin defa dönen bir loopun olduğu methodu çalıştırıyor.
Ancak burada sorun şu, t1 ya da t2 thread’leri T anında counter’ın aynı değerini bilemiyorlar. Yani t1 thread’i için counter 30 ise ve t2 için de 28 ise problemler ortaya çıkabiliyor.
Çünkü t1 31 olacakken, t2 geliyor ve counter’ı 29 yapıyor. Bendeki çıktıda örnek şöyle;

Peki bu kodu nasıl düzeltip thread-safe bir hale getirebiliriz? Cevabı basit aslında. Bunun için lock keywordünü kullanıyoruz. Thread-safe olmasını istediğimiz bölgeyi lock scope’u içerisine alıyoruz.
Yeni kodumuz şöyle oluyor
class Program
{
private static int counter = 0;
static object lockObject = new object();
static void Main(string[] args)
{
Thread t1 = new Thread(SayiyiBirerBirerArttir);
Thread t2 = new Thread(SayiyiBirerBirerArttir);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine($"Sayının güncel değeri: {counter}");
}
private static void SayiyiBirerBirerArttir()
{
for (int i = 0; i < 10000; i++)
{
lock (lockObject)
{
counter++;
}
}
}
}
Burada lock keywordünün görevi, sadece bir thread’in aynı anda counter değişkenine erişmesini güvence altına almak. Bu sayede aynı anda aynı bellek bölgesine erişimlerden kaynaklı senkronizasyon problemini gidermiş oluyoruz. Bu tabiiki ilk yöntemdi. Bir başka yöntem de SemaphoreSlim kullanmak.
SemaphoreSlim de yine belirli sayıda thread’in aynı anda çalışmasına izin verirken, burada biraz daha configure edilebilir bir semaphore mekanizması yer alıyor.
class Program
{
private static int counter = 0;
static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1);
static void Main(string[] args)
{
Thread t1 = new Thread(SayiyiBirerBirerArttir);
Thread t2 = new Thread(SayiyiBirerBirerArttir);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine($"Sayının güncel değeri: {counter}");
}
private static void SayiyiBirerBirerArttir()
{
semaphoreSlim.Wait();
for (int i = 0; i < 10000; i++)
{
counter++;
}
semaphoreSlim.Release();
}
}
SemaphoreSlim constructor’ında 2 tane parametreye sahip. Bunlar initialCount ve maximumCount. Yani aynı anda kaç tane ve en fazla kaç tane olabileceğinin belirtiyor. Yani anlamamız gereken şey, aynı anda kaç thread’in ilgili nesneye erişeceğidir.
Umarım bu kısmı anlamışızdır çünkü yazarken parmaklarım ağrıdı 😄
Kaynak Paylaşımı Ne Peki?
https://www.baeldung.com/cs/threads-sharing-resources
Kaynak paylaşımı, aynı resource’ları kullanarak çalışan thread’lerin ya da processlerin birbirleriyle uyumlu bir şekilde çalışmasını sağlar. Mesela birden fazla thread’in veri tabanına ulaşmaya çalıştığı bir senaryoyu düşünebiliriz. Bu sayede hangi kaynağın ne zaman kullanılabileceği gibi sorunların da önüne geçilmiş olur. Eğer kaynak paylaşımı doğru bir şekilde ele alınmaz ise data corruption yani veri bozulması gibi sorunlarla karşılaşılabilir.
Yine kaynak paylaşımı ile ortaya çıkan bilinmesi gerekenler ise şunlardır;
- locking
- race condition
- signaling
- monitor
Burada locking ne yapıyor görmüştük.
Race Condition
Yine race condition ise bir başka yazımda bahsettiğim gibi;
aynı anda en az 2 thread’in, aynı dataya eriştiği ve en azından birinin bu dataya veri yazmaya çalıştığı anda ortaya çıkabilir
Signaling
Signaling ise elimizde bir thread var diyelim. Bu thread bir resource’u kullanmak için bekliyor. Bu resource’u o anda kullanan thread release işlemi yaptığı anda, bekleyen thread’e “ben bunu serbest bıraktım” şeklinde bir sinyal gönderir. Bunu C# dilinde AutoResetEvent ya da ManuelResetEvent ile yapabilirsiniz. Mesela AutoResetEvent için bir örnek verelim;
namespace Senkronizasyon;
class Program
{
static AutoResetEvent autoResetEvent = new AutoResetEvent(false);
static void Main(string[] args)
{
Thread t1 = new Thread(BirIsYap);
t1.Start();
Console.WriteLine("Ana thread beklemede...");
autoResetEvent.WaitOne();
Console.WriteLine("Ana thread çalışmaya devam ediyor");
}
static void BirIsYap()
{
Console.WriteLine("Thread bir şeyler yapıyor");
Thread.Sleep(3000);
Console.WriteLine("Thread yaptığı işi bitirdi");
autoResetEvent.Set();
}
}
Eğer aşağıdaki kodu kapatacak olsaydık, ana thread’in çalışmaya devam ettiğini bildiren mesajı göremeyecektik;
autoResetEvent.Set();
Monitor
Monitor işlemi ise thread’lerin senkronizasyonu için kullanılır. Bir nesne üzerinde locking sağlar ve bu nesneye ait kod bloklarının sadece bir thread tarafından çalıştırılmasını garanti eder. Bu sayede, kaynak paylaşımı ve senkronizasyon işlemleri kontrol edilebilir.
Buna dair bir kod paylaşmayacağım. Ana konuda kopmak istemiyorum. Ama kaynak paylaşımı konusunu da anladık gibi fakat bir banka hesabı örneği verelim mi?
namespace Senkronizasyon;
class Program
{
static void Main(string[] args)
{
BankaHesabi bankaHesabi = new BankaHesabi();
Task t1 = Task.Factory.StartNew(() => bankaHesabi.ParaCek(100));
Task t2 = Task.Factory.StartNew(() => bankaHesabi.ParaCek(200));
Task t3 = Task.Factory.StartNew(() => bankaHesabi.ParaYatir(300));
Task.WaitAll(t1, t2, t3);
Console.WriteLine($"Hesap Bakiyesi: {bankaHesabi.Bakiye}");
}
}
class BankaHesabi
{
private decimal _bakiye;
private readonly object _bakiyeLockNesnesi = new object();
public decimal Bakiye => _bakiye;
public void ParaYatir(decimal tutar)
{
lock (_bakiyeLockNesnesi)
{
_bakiye += tutar;
Console.WriteLine($"Yatırılan para: {tutar}. Hesap Bakiyesi: {_bakiye}");
}
}
public void ParaCek(decimal tutar)
{
lock (_bakiyeLockNesnesi)
{
if (_bakiye >= tutar)
{
_bakiye -= tutar;
Console.WriteLine($"Çekilen tutar: {tutar}. Hesap Bakiyesi: {_bakiye}");
}
else
{
Console.WriteLine(
$"Çekmeye çalıştığınız ₺{tutar} hesap bakiyesinin yetersizliği nedeniyle çekilemedi.");
}
}
}
}
Yukarıdaki örnekte, birden fazla thread’e sahibiz ve bu thread’ler hem para yatırıyor hem de para çekiyorlar. Eğer kaynakların paylaşımı doğru şekilde yapılmamış olsaydı muhtemelen bir noktada hesaplar karışacaktı.
Bir başka örnek de şöyle olabilir;
using System;
using System.Threading;
class Program
{
static ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();
static int kaynak = 0;
static void Main(string[] args)
{
Thread t1 = new Thread(new ThreadStart(ResourcetanVeriOku));
Thread t2 = new Thread(new ThreadStart(ResourceaVeriYaz));
Thread t3 = new Thread(new ThreadStart(ResourcetanVeriOku));
Thread t4 = new Thread(new ThreadStart(ResourceaVeriYaz));
t1.Start();
t2.Start();
t3.Start();
t4.Start();
t1.Join();
t2.Join();
t3.Join();
t4.Join();
}
static void ResourcetanVeriOku()
{
rwLock.EnterReadLock();
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} nolu thread {kaynak} nolu resource'tan veri okuyor");
Thread.Sleep(1000);
rwLock.ExitReadLock();
}
static void ResourceaVeriYaz()
{
rwLock.EnterWriteLock();
kaynak++;
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} nolu thread {kaynak} nolu resource'a veri yazıyor");
Thread.Sleep(1000);
rwLock.ExitWriteLock();
}
}
Yukarıdaki örnekte 2 tane okuma 2 tane de yazma işlemi yapan threade sahibiz. Ve bunların hepsi paylaşılan bir kaynağı okur ve bu kaynağa yazarlar. Yazma ve okuma methodlarının içlerindeki methodları açıklamayacağım. İsim olarak okurken ya da yazarken lock işlemi yaptığını ve release ettiğini görüyoruz.
ReaderWriterLockSlim, aynı anda birden fazla okuma işlemine izin verirken, yalnızca tek bir yazma işlemine izin verir. Bu açıklama bize, bu sınıfın performans açısından avantajlı olduğunu söyleyebilir. Çünkü aynı anda sadece bir write işlemi yapılırken, birden fazla read işlemi yapılabilir.
Özet Geç
Şimdi özetlemek gerekirse;
Özetle, senkronizasyon ve kaynak paylaşımı concurrency sorunlarının çözümünde önemli bir role sahiptir. Senkronizasyon, thread’lerin ya da process’lerin birbirleriyle uyumlu bir şekilde çalışmasını sağlamak için kullanılır.
Kaynak paylaşımı, aynı kaynakları kullanarak çalışan thread’lerin ya da process’lerin birbirleriyle uyumlu bir şekilde çalışmasını sağlar. Bu iki kavramın doğru bir şekilde anlaşılması, concurrent programlamada başarılı bir şekilde çalışmayı sağlamak için önemlidir.
Okuduğunuz için teşekkür ederim. Umarım faydalı bir yazı olmuştur. Eksiklerim ya da yanlış bilgilendirdiklerim varsa şimdiden kusura bakmayın.