Senkron & Asenkron ve MultiThread Programlama -3 — Semaphore — SemaphoreSlim
Race Condition & Senkronizasyon Teknikleri -2 Semaphore — SemaphoreSlim
CancellationTokenSource
- CancellationTokenSource asenkron süreçleri iradeli bir şekilde sonlandırmamızı sağlayan bir nesnedir.
- Genellikle, bir iş parçacığının çalışması sırasında bir kullanıcı veya sistem olayı nedeniyle iş parçacığının iptal edilmesi gerektiğinde kullanılır.
Thread thread = new((cancellationToken) =>
{
var cancel = (CancellationTokenSource)cancellationToken;
while (true)
{
if (cancel.IsCancellationRequested) break;
Console.WriteLine("calisti..");
}
Console.WriteLine("Thread görevini tamamladı.");
});
CancellationTokenSource cancellationToken = new();
thread.Start(cancellationToken);
Thread.Sleep(5000);
cancellationToken.Cancel();
- Yukarıdaki örnekte bir cancellationToken nesnesi oluşturulmuş ve ilgili thread Start edilirken parametre olarak gönderilmiştir. 5 saniye thread sleep edildikten sonra cancellationToken.Cancel(); şeklinde ilgili thread’in durdurulması bilgisi gönderilmiştir.
Spinning
- C# thread senkronizasyonunda spinning, bir thread’in başka bir thread’in kilitli bir kaynağı (örneğin bir shared variable) kullanılabilir hale gelmesini sürekli olarak kontrol etme işlemidir.
- Spinning thread’leri belirli bir koşula karşın döngü ile bekletmeyi yani bloklatmayı sağlayan bir davranışı ifade eder.
- Bu yaklaşım, beklenen koşul gerçekleşene kadar thread’in aktif bir şekilde çalışmasını ve diğer thread’lere geçiş yapmamasını sağlar.
- Bu davranış
spinning
veyabusy-waiting
olarak adlandırılır.
bool threadCondition = true; //ortak değişken
Thread thread1 = new(() =>
{
while (true)
{
if (!threadCondition)
{
for (int i = 1; i <= 10; i++)
Console.WriteLine($"Thread 1 {i}.");
threadCondition = false;
break;
}
}
});
Thread thread2 = new(() =>
{
while (true)
{
if (threadCondition)
{
for (int i = 10; i > 0; i--)
Console.WriteLine($"Thread 2 {i}.");
threadCondition = false;
break;
}
}
});
- Yukarıdaki örnekte görüldüğü üzere, ortak oluşturulmuş
threadCondition
değeri üzerinden, sonsuz döngü içerisindeki thread işlemleri birinci ve ikinci thread’de birbirini engellemeyecek veya race condition oluşturmayacak şekilde kontrol altına alınmıştır. - Spinning yöntemi, içerisindeki sonsuz döngü mantığı sebebiyle zaman zaman fazla işlemci kullanımına sebep olabilir.
- Bu yüzden işlemci kaynaklarının etkili bir şekilde kullanılabilmesi için uzun süreli beklemelerde veya yüksek talep durumlarında spinning davranışı önerilmemektedir.
Monitor.Enter & Monitor.Exit
- Monitor.Enter & Monitor.Exit metotları, locking mekanizmasının fonksiyonel versiyonlarıdır.
- Monitor.Enter, aynı lock mekanizmasında olduğu gibi belirli bir nesnenin üzerinden kilit almaya çalışmaktadır. Eğer başka bir thread bu kilidi almışsa beklemeye alınacaktır.
- Monitor.Exit ise Monitor.Enter ile kilitlenmiş olan bu nesneyi serbest bırakır.
Esasında locking mekanizması da arka planda Monitor.Enter & Monitor.Exit metotlarını kullanır.
object locking = new(); //locking object
int i = 0;
Thread thread1 = new(() =>
{
try
{
Monitor.Enter(locking);
for (i = 0; i < 10; i++)
Console.WriteLine($"Thread 1 {i}");
}
finally
{
Monitor.Exit(locking);
}
});
Thread thread2 = new(() =>
{
try
{
Monitor.Enter(locking);
for (i = 0; i < 10; i++)
Console.WriteLine($"Thread 2 {i}");
}
finally
{
Monitor.Exit(locking);
}
});
thread1.Start();
thread2.Start();
- Örnekte de dikkat edeceğiniz üzere Monitor metotları kullanılırken thread içerisindeki işlemin uzun sürebilmesi ve diğer thread’leri de bu uzun süren işlemin etkilemesine önlemek için Monitor.Exit metodunu try-catch mekanizmasının finally bloğu içerisine almak kritik önem arz eder.
LockTaken
- Çok nadiren de olsa, Monitor.Enter metodunun ilgili kilitleme işlemini yapamaması söz konusu olabilmektedir.
- Haliyle bu durumu anlayabilmek ve kontrol edebilmek için
LockTaken
parametresini kullanabiliriz. - Yani LockTaken sayesinde ilk kilidin başarıyla alınıp alınmadığı kontrol edilmektedir. Bu durum kritik bölgelerde güvenli bir şekilde kilitleme işlemlerinin olup olmadığının kontrolü açısından büyük önem arz etmektedir.
object locking = new();
int i = 0;
Thread thread1 = new(() =>
{
try
{
bool lockTaken = false;
Monitor.Enter(locking, ref lockTaken);
if (lockTaken)
for (i = 0; i < 10; i++)
Console.WriteLine($"Thread 1 {i}");
}
finally
{
Monitor.Exit(locking);
}
});
Thread thread2 = new(() =>
{
try
{
bool lockTaken = false;
Monitor.Enter(locking, ref lockTaken);
if (lockTaken)
for (i = 0; i < 10; i++)
Console.WriteLine($"Thread 2 {i}");
}
finally
{
Monitor.Exit(locking);
}
});
thread1.Start();
thread2.Start();
- Örnekte görüldüğü gibi, Monitor.Enter fonksiyonu, eğer başarılı bir kilitleme işlemi yaptıysa oluşturduğumuz lockTaken referansına True değer döndürecektir. Ardından yazacağımız if bloğunda da bu referans değeri kullanarak kontrolümüzü yapabiliriz.
Monitor.TryEnter
- Monitor.TryEnter bir nesne üzerinde lock almayı deneyen ve alınıp alınmadığını kontrol eden bir senkronizasyon yöntemidir.
- Bu metot ile bir kodu kilitlemeye çalışırken rekabet eden diğer thread’lerle ilişkili riskleri
millisecondTimeout
parametresi sayesinde kontrol edilebilmektedir. - Bir kilitleme isteği verilen milisaniye cinsinden süre zarfında kilitlenmeye çalışılır ve eğer bu süre içerisinde kilitleme gerçekleştirilirse başarılı yani true bir sonuç döner. Aksi takdirde bu sonuç false olacaktır.
object locking = new();
int i = 0;
Thread thread1 = new(() =>
{
var result = Monitor.TryEnter(locking, 100);
if (result)
try
{
for (i = 0; i < 10; i++)
Console.WriteLine($"Thread 1 {i}");
}
finally
{
Monitor.Exit(locking);
}
});
Thread thread2 = new(() =>
{
var result = Monitor.TryEnter(locking, 10);
if (result)
try
{
for (i = 0; i < 10; i++)
Console.WriteLine($"Thread 2 {i}");
}
finally
{
Monitor.Exit(locking);
}
});
thread1.Start();
thread2.Start();
- Yukarıda Görüldüğü üzere, Monitor.TryEnter metodunda her iki thread’de de milisaniye cinsinden parametre verilmiştir. Örneğin thread2’de kilitleme işlemi 10 milisaniyeden fazla sürerse result değeri false olacak ve kilitleme işlemi başarısız olacaktır.
- TryEnter bu yeteneği sayesinde Bizler kilit alınamadığı takdirde beklemek yerine hemen farklı işlemlere devam etme imkanı vermektedir.
Monitor.TryEnter metodunda aynı zamanda 3. parametre olarak yukarıda bahsettiğimiz lockTaken parametresi de vardır.
Mutex Sınıfı
- Mutex sınıfı da lak mekanizmasına benzer şekilde fakat bilgisayar seviyesinde process’ler arası kilitleme işlemi yürütebilen bir senkronizasyon yapısıdır.
- İsmi Mutual Exclusion’dan gelir.
Mutex mutex = new(); //Mutex object
Thread thread1 = new(() =>
{
mutex.WaitOne();
for (int i = 0; i < 10; i++)
{
Console.WriteLine($"Thread 1 {i}");
}
mutex.ReleaseMutex();
});
Thread thread2 = new(() =>
{
mutex.WaitOne();
for (int i = 0; i < 10; i++)
{
Console.WriteLine($"Thread 2 {i}");
}
mutex.ReleaseMutex();
});
thread1.Start();
thread2.Start();
Semaphore & Semaphore Slim
- Semaphore & SemaphoreSlim kavramları da, kaynakları olan erişimin kontrolünde kullanılan senkronizasyon tekniklerinden önemli bir tanesidir.
- Daha önceki bahsedilen senkronizasyon teknikleri gibi Semaphore & SemaphoreSlim ile de amaçlanan şey, belirli bir kod kritik bölgesine aynı anda giren thread’lerin kontrolünü sağlayabilmektir.
- Fakat Semaphore & SemaphoreSlim diğer senkronizasyon tekniklerine göre daha esnek bir yapıya sahiptir.
Önceki farklı senkronizasyon tekniklerinde, genellikle yalnızca bir thread’in kritik bölgeye erişimi yaklaşımı üzerine kullanılır. Fakat Semaphore & SemaphoreSlim kullanımı ile bir kritik bölgeye parametrik olarak bir yada birkaç thread’in girmesine izin verilebilir.
- Semaphore & SemaphoreSlim senkronizasyon tekniğinde koddaki kritik bölgeye parametrik olarak belirlenen thread sayısı kadar thread’in erişimine izin verilir. Sıradaki diğer thread’ler bekletilir. Bir thread kritik bölgedeki işini bitirip çıktıktan sonra yeni bir thread bölgeye kabul edilir.
- Bu işlemi şöyle örnekleyebiliriz; 4 koltuğu olan bir araba olduğunu ve 4 kontuğunun da dolu olduğunu düşünün. Arabanın dışında da o arabayı binmek için tekrar olduğunu düşünün. Arabanın dışında bekleyen kişilerin arabaya oturabilmesi için oturanlardan en az bir tanesinin kalkması ve o koltuğu boşaltması gerekir.
- Semaphore ve SemaphoreSlim kavramları, her ikisi de belirli bir kaynağa belirli sayıdaki thread’lerin eş zamanlı olmak her işim var kontrol etmekte kullanılsa da birbirinden bazı farkları vardır:
Semaphore
- Semaphore, SemaphoreSlim’e nazaran daha eski bir senkronizasyon aracıdır.
- .Net 2.0 ile beraber gelmiştir.
- İşletim sistemi kaynaklarına bağlıdır. Bu yüzden işletim sistemi tarafından yürütülen bir sınıftır.
- Senkron davranış sergiler.
SemaphoreSlim
- Semaphore’a göre daha yeni ve daha hafif bir yapıya sahiptir.
- .Net 4.0 ile beraber gelmiştir.
- Semaphore’a mesela daha hızlı performanslı çalışmakta, ve buna rağmen semaphore’a göre daha düşük bellek kullanımına sahiptir. Yani daha az maliyetlidir.
- Işletim sistemine bağlı değildir dolayısıyla daha hızlı ve atiktir.
- Hem Senkron hem de Asenkron davranış sergileyebilmektedir.
SemaphoreSlim’in Semaphore’a göre daha az maliyetli ve daha performanslı çalışabilmesinin sebebi, Aslında işletim sistemine bağlı olmaması ve .Net seviyesinde bir yürütme mekanizmasına sahip olmasıdır.
Kağıt üstünde semaphoreSlim, semaphore’a göre daha iyi görünse de, ihtiyaca yönelik gerektiğinde Semaphore senkronizasyon tekniğini kullanmak gerekir.
List<int> numbers = new();
Semaphore semaphore = new(2, 3); //1.parametre=> izin verilen thread sayısı 2.kritik bölgeye erişebilecek max thread sayısı
Thread thread1 = new(() =>
{
semaphore.WaitOne(); //thread erişim izni ister 2 izinden 1 tanesini alır.
int i = 0;
while (i < 10)
{
Console.WriteLine($"Thread1 {++i}");
numbers.Add(i);
Thread.Sleep(1000);
}
semaphore.Release(); //işlemini tamamlayan thread tekrar yerini boşaltır.
});
Thread thread2 = new(() =>
{
semaphore.WaitOne();
int i = 10;
while (i < 20)
{
Console.WriteLine($"Thread2 {++i}");
numbers.Add(i);
Thread.Sleep(1500);
}
semaphore.Release();
});
Thread thread3 = new(() =>
{
semaphore.WaitOne();
int i = 20;
while (i < 30)
{
Console.WriteLine($"Thread3 {++i}");
numbers.Add(i);
Thread.Sleep(2000);
}
semaphore.Release();
});
thread1.Start();
thread2.Start();
thread3.Start();
//output
Thread2 11
Thread1 1
Thread1 2
Thread2 12
Thread1 3
Thread2 13
Thread1 4
Thread1 5
Thread2 14
Thread1 6
Thread2 15
Thread1 7
Thread1 8
Thread2 16
Thread1 9
Thread2 17
Thread1 10
Thread3 21
Thread2 18
Thread2 19
Thread3 22
Thread2 20
Thread3 23
Thread3 24
Thread3 25
Thread3 26
Thread3 27
Thread3 28
Thread3 29
Thread3 30
- Yukarıdaki örnekte 3 farklı thread kullanılarak bir semaphore kullanım örneği verilmiştir.
- Örnekteki notta da görüldüğü üzere semaphore örneği oluştururken verilen 1. parametre thread’lere izin verilecek max sayıyı belirtir. 2. parametre de bu semaphore ile kritik bölgelere aynı anda erişim sağlayabilecek max izin verilen thread sayısını belirtir.
- Çıktıda görüldüğü gibi, semaphore başlangıc parametrelerinde kritik bölgeye erişim parametrisi 3 ve izin verilecek thread sayısı parametresinde 2 verildiği görülüyor. Çıktıda da bu parametreler doğrultusunda thread2 ve thread1 max erişim parametresi 3 olmasına rağmen izin verilen parametre sayısı 2 olduğu için bu ikisi işlemine başlamış, thread1 işlemini bitirdikten sonra thread3 işlemine başlamıştır.
Burada 1. parametre olan max izin verilme parametresinin 2. maxCount parametresinden büyük olması hataya yol açacaktır. Çünkü mantıken izin verilen Max thread sayısından daha fazla thread’e kritik bölüme erişim izni verilmeye çalışılmış olacaktır.
List<int> numbers = new();
using SemaphoreSlim semaphoreSlim = new(3, 3);
Thread thread1 = new(() =>
{
semaphoreSlim.Wait(100);
int i = 0;
while (i < 10)
{
Console.WriteLine($"Thread1 {++i}");
numbers.Add(i);
Thread.Sleep(100);
}
semaphoreSlim.Release();
});
Thread thread2 = new(async () =>
{
await semaphoreSlim.WaitAsync(1000);
int i = 10;
while (i < 20)
{
Console.WriteLine($"Thread2 {++i}");
numbers.Add(i);
Thread.Sleep(100);
}
semaphoreSlim.Release();
});
Thread thread3 = new(() =>
{
semaphoreSlim.Wait(5000);
int i = 20;
while (i < 30)
{
Console.WriteLine($"Thread3 {++i}");
numbers.Add(i);
Thread.Sleep(100);
}
semaphoreSlim.Release();
});
thread1.Start();
thread2.Start();
thread3.Start();
//output
Thread3 21
Thread1 1
Thread2 11
Thread1 2
Thread2 12
Thread3 22
Thread1 3
Thread2 13
Thread1 4
Thread3 23
Thread1 5
Thread2 14
Thread1 6
Thread3 24
Thread2 15
Thread1 7
Thread1 8
Thread2 16
Thread3 25
Thread1 9
Thread2 17
Thread1 10
Thread3 26
Thread2 18
Thread3 27
Thread2 19
Thread2 20
Thread3 28
Thread3 29
Thread3 30
- Bu örnekte de semaphoreSlim kullanımına örnek verilmiştir.
- initialCount 3 ve maxCount parametreleri verilerek aynı anda 3 thread’in de semaphore’dan izin alarak işlemlerini gerçekleştirdiği gözlemlenmiştir.
- Burada thread2’de gözlemlendiği gibi, semaphoreSlim’de Asenkron bir yaklaşım da kullanılabilir.
Wait aksiyonuna milisaniye cinsinden bir parametre vererek, bir thread için Max bir izin bekleme süresi atayabiliriz. Bu thread verilen süre kadar bekler ve eğer hala semaphore’dan bir izin alamadıysa kritik bölgeye erişim sağlar ve işlemini gerçekleştirir.
semaphoreSlim.Wait(1000);
Kullanılan araç ister semaphore, ister semaphoreSlim olsun, her ikisinde de, İşimiz bittiği takdirde tüketilen kaynakların serbest bırakılması ve ve bellek yönetiminin sağlanması için
semaphore.Dispose();
ile semaphore serbest bırakılmalı veya semaphore & semaphoreSlim işlemiusing
bloğu içerisinde kullanılmalıdır.