Multithread Uygulamalarda Değişken Kullanımı

Yaklaşık 3 aydır “Sobee Studios” firmasında çalışıyorum ve açık söylemek gerekirse bu kadar kısa sürede bir çok şey öğrendim. Teşekkürü sonda yapılmasını pek doğru bulmadığım için başında yapayım, bu 3 aylık süreçte deneyimlerini benimle paylaşan Cem Sermen ve Fatih Güngör‘e çok teşekkür ediyorum. Ayrıca uzun zamandır yazmadığım için de baya bir konu birikti, en kısa sürede bunları burada paylaşacağımı söyleyip konuya gireyim.

Bir çok uygulamada özellikle Client-Server uygulamalarında ram’de geçici olarak veri tutmanız gerekebilir. Bu verileri bir değişkende (int, bool, long vs.) tutabileceğiniz gibi birden fazla olması durumunda dizilerde de tutabilirsiniz. SingleThread bir uygulama da diziye veri eklemek ve çıkartmak sizin için problem olmayacaktır çünkü o diziye sadece tek bir thread’den tek bir uygulama erişecektir. Sorun multithread uygulamalar da ortaya çıkıyor. 

Resim üzerinden gidecek olursak, Thread-1 diziye bir veri elemanı eklemek için erişiyor olsun. Aynı şekilde Thread-2’de diziden veri okumak için erişiyor olduğunu düşünelim. Önce hangi işlem yapılacak? Okuma işlemi mi? Yoksa yazma işlemi mi? Yada Thread-1 veri’deki bir elemanı değiştiriyor olsun, Thread-2’nin okumaya çalıştığı veri elemanının güncel olmasını garanti edebilir miyiz?

İşte multithread programlama da bu tür sorunlar karşımıza çıkabilmektedir ve özellikle dizi erişimlerinde bunlara çok dikkat edilmesi gerekiyor. Uygulamanızın her hangi bir anında birden fazla thread aynı veri elemanını kullanmak isteyebilir. O veri elemanını kullanan thread’in güvenli bir şekilde veriyi erişip kullanabilmesini bizim garanti ediyor olmamız lazım. Peki bunu nasıl yapacağız? 

.Net’de lock anahtar kelimesi tam da bu iş için kullanılıyor. Siz bir veri elemanına erişirken lock anahtar kelimesi ile eriştiğinizde diğer thread’ler sizin işleminizi bitirmenizi bekler. İşleminiz bittiği zaman sıradaki thread diziyi kullanmaya devam eder. 

List dizi = new List();
// Diğer threadler'in dizi'ye erişememesi 
// lock anahtar sözcüğünü kullanıyoruz.
lock (dizi)
{
   // Bu nokta da dizi, diğer 
   // threadler tarafından kullanılamaz.
   dizi.Add(1);
}
// Dizi artık diğer threadler tarafından
// kullanılır durumda.
 

Yukarıda ki kod parçacığında görüldüğü üzere lock anahtar kelimesi ile bir veri elemanının diğer threadler tarafından erişilmesini kolaylıkla engelleyebiliyoruz. Bu şekilde sistemimiz sorunsuz bir şekilde çalışır çalışmasına ama ne kadar performanslı çalışır? Sistemin çalışmasını bir kez daha inceleyelim.

Bir thread diziye okuma, yazma veya güncelleme yapmak istediğinde o diziyi lock anahtar kelimesi ile kilitliyor ve diğer threadler kullanamıyor. Şimdi sormamız gereken asıl soru şu; İki tane okuma yapmak için diziye erişmek isteyen thread olsun, bir thread’in okuma işlemi bitmeden diğeri okuyamayacak. Böyle bir durumda lock işlemi ne kadar mantıklı olur? Okuma yapamya çalışan bir thread gereksiz yere diğer thread’i bekliyor. Bu işlemin sisteme maliyeti; [okuma süresi] x [ThreadSayısı]. Halbuki okuma yapanlar için ayrı bir kuyruk, yazma yapanlar için ayrı bir kuyruk olsa sistem daha da performanslı çalışmaz mı? 

Resimde gösterildiği gibi bu threadlerin hepsi aynı anda olabilir. Okuma zamanında diziyi yada herhangi bir veri elemanını lock anahtar kelimesi ile kilitlemek performans açısından iyi bir teknik değil. Uygulamamızın tam olarak çalışma/zaman grafiğini çizecek olursak;

Şemayı biraz yorumlayalım; Thread-1 t=0 anında işleme alınıyor ve t=2 anında işlem sonlandırılıyor. t=2 anında Thread-2 Yazma, Thread-3 Okuma ve Thread-4 Okuma threadleri gelsin hangi işlemin önce yapılacağının garantisi yok ama ben önce yazma threadinin yapıldığını varsayıyorum. Bu durumda yazma threadi işlemini bitirene kadar okuma threadleri (Thread-3 ve Thread-4) bekliyorlar. Yazma threadi işlemini bitirdikten sonra okuma threadleri aynı anda işlemlerini yapmaları lazım çünkü okuma threadlerinin lock işlemleri birbirlerini etkilememesi lazım. 

Özet olarak okuma threadlerinin lock işlemleri ile yazma threadlerinin lock işlemlerini ayırdığımız zaman performans olarak daha iyi çalışmış oluyor. Sistemimizi klasik lock anahtar kelimesi ile yapsaydık bu işlem t=12 süresinde bitecekti. Multithread uygulamanızda okuma işlemi ağırlıktaysa bu teknik ile performansı ciddi oranda arttırabilirsiniz ama yazma işlemini çok sık yapıyorsanız pek bir performans kazancınız olmaz.

Burak iyi güzel anlatıyorsun da bu son dediğinin implementasyonunu nasıl yapacağız? Bu sorunun cevabı; ReaderWriterLockSlim Class

Bu sınıf tam olarak anlattığım işi yapıyor. Tanım kısmına baktığınız da şu yazıyı göreceksiniz; “Represents a lock that is used to manage access to a resource, allowing multiple threads for reading or exclusive access for writing.” Anlamı ise; kim ki bu sınıfı kullanır, multithread uygulamalarda kaynağı yönetmek için kullanılan kilitleme (lock) işlemlerinde yazma yapıyorsa tek tek, okuma yapıyorsa çoklu şekilde yapabilir. MSDN’de bu sınıfı çok güzel anlatmış (sanki kötü anlattığı sınıf varda 🙂)ben sadece temel olarak bir kaç fonksiyonundan bahsedeceğim.

public void EnterReadLock() fonksiyonu bir veri elemanından okuma yapacaksanız çağırmanız gereken fonksiyon. Bu fonksiyonu çağırdığınız zaman uygulamanızda artık sadece okuma threadlerine izin verebilirsiniz. Yazma threadleri bu fonksiyon “release” olana kadar bekleyecektir. Peki release işlemini nasıl gerçekleştireceğiz oda tahmin edebileceğiniz gibi public void ExitReadLock() fonksiyonu ile yapılabilir. Bu iki fonksiyonun kullanımına örneği MSDN’den alalım;

private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
private Dictionary innerCache = new Dictionary();

...

public string Read(int key)
{
    cacheLock.EnterReadLock();
    try
    {
        return innerCache[key];
    }
    finally
    {
        cacheLock.ExitReadLock();
    }
} 
 

Aynı şekilde eğer yazma işlemi yapılacaksa sınıfın yine public void EnterWriteLock() ve public void ExitWriteLock() fonksiyonları kullanılmalıdır. Bu fonksiyonlar için MSDN örneği ise;


private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
private Dictionary innerCache = new Dictionary();

...

public void Add(int key, string value)
{
    cacheLock.EnterWriteLock();
    try
    {
        innerCache.Add(key, value);
    }
    finally
    {
        cacheLock.ExitWriteLock();
    }
} 

Diyelim ki kodunuz da okuma mı? yoksa yazma mı? yapacağınızı bilmiyorsunuz. Bir fonksiyondan gelecek cevaba göre ya bir okuma yada bir yazma yapacaksınız, böyle bir durumda ne yapacağız? .Net bunu da düşünmüş ve bu sınıfa iki tane daha fonksiyon eklemiş; public void EnterUpgradeableReadLock() ve public void ExitUpgradeableReadLock() burada işlem biraz daha farklı işliyor eğer siz Upgradeable bir fonksiyonu çağırıyorsanız bu fonksiyon bir sonraki satır Okuma veya Yazma moduna geçebilir. MSDN’deki örnek üzerinden devam edelim;

private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
private Dictionary innerCache = new Dictionary();

...

public AddOrUpdateStatus AddOrUpdate(int key, string value)
{
    cacheLock.EnterUpgradeableReadLock();
    try
    {
        string result = null;
        if (innerCache.TryGetValue(key, out result))
        {
            if (result == value)
            {
                return AddOrUpdateStatus.Unchanged;
            }
            else
            {
                cacheLock.EnterWriteLock();
                try
                {
                    innerCache[key] = value;
                }
                finally
                {
                    cacheLock.ExitWriteLock();
                }
                return AddOrUpdateStatus.Updated;
            }
        }
        else
        {
            cacheLock.EnterWriteLock();
            try
            {
                innerCache.Add(key, value);
            }
            finally
            {
                cacheLock.ExitWriteLock();
            }
            return AddOrUpdateStatus.Added;
        }
    }
    finally
    {
        cacheLock.ExitUpgradeableReadLock();
    }
}
...

public enum AddOrUpdateStatus
{
    Added,
    Updated,
    Unchanged
}; 

Fonksiyonun isminde de anlaşılacağı üzere yapılacak işlem ya bir update yada ekleme. Parametre olarak geçilen değer dizide varsa bir yazma işlemi yapılacaktır ve o zaman modu EnterWriteLock() diyerek güncelleyeceğiz. Çıkarken de eğer EnterWriteLock() dediysek ExitWriteLock() dememiz yeterli, modu hiç değiştirmediysek ExitUpgradeableReadLock() dememiz lazım.

Sonuç; Multithread programlama yaparken bir çok parametreyi dikkate alıp ona göre kod yazmak gerekiyor bunlardan bir tanesi de kaynak kullanımı. Hangi işlemin hangi sırada ve ne yapılacağını bilmiyorsunuz bunun önlemini alarak kodlamak gerekiyor. 

NOT: Kod örneklerindeki son satırı dikkate almayın kullandığım eklentinin bir bug’ı. “<” işareti ile açılan tag’leri kapatmaya çalışmış ondan o satırı ekliyor.


2 Responses to Multithread Uygulamalarda Değişken Kullanımı

    Bir cevap yazın

    Your email address will not be published. Please enter your name, email and a comment.