18 Nisan 2015 Cumartesi

Java ve Concurrency (=Threads)

https://docs.oracle.com/javase/tutorial/essential/concurrency/index.html

Bir uygulama birden fazla şeyi aynı anda yapabiliyorsa buna concurrent uygulama denir. Concurrency kavramı aynı anda birden fazla işlem yapabilme yeteneğini ifade eder.
java.util.concurrent paketi bu amaçla kullanılır.


Processes and Threads


Concurrent programlamada iki tane çalışma birimi vardır: process ve thread.
Java'da concurrency thread'lerle sağlanır.

Bir tek işlemci çekirdeğinde dahi birden fazla process ve thread zaman paylaşımı yaparak aynı anda çalışma izlenimi uyandırır. Buna işletim sistemi terminolojisinde time slicing denir.

Process

Her processin kapalı bir çalışma ortamı, yani kendine özel olarak ayrılan bir bellek alanı vardır.
Kullanıcının tek bir uygulama olarak gördüğü şey aslında birden fazla processin birlikte çalışmasının ürünü olabilir. İşletim sistemi processler arası iletişimi sağlamak için Inter-Process Communication (IPC) kaynaklarını, yani pipe, socket,vb kullanır. IPC aynı zamanda farklı sistemlerdeki processlerin de iletişimi sağlar.

JVM implementasyonları çoğunlukla tek processten oluşur. ProcessBuilder nesnesi ile bir jva uygulamasına processler eklenebilir.

Thread

Hem processler hem de threadler, ikisi de çalışma ortamı sağlarlar. Fakat yeni bir thread yaratmak için gereken kaynaklar daha azdır. Bu yüzden threadlere hafif processler de denir.

Threadler processlerin içinde varolurlar. Her processte en az bir tane thread vardır.
Bir processin kaynaklarını (örneğin bellek alanı ve açılmış olan dosyalar gibi) threadler ortaklaşa kullanırlar. Bu yüzden çok hesaplı fakat bazen de problemli bir iletişimleri vardır.

Her processte ilk olarak tek bir main thread bulunur. Bu threadi kullanarak yeni threadler yaratabiliriz.

Thread objects


Her thread, Thread classından yaratılan bir instance'a bağlıdır.
Thread kullanarak multi-threaded uygulama yapmanın iki yöntemi vardır:
1. Threadleri direkt olarak yaratarak yönetimini yapmak
Yani uygulamanın asenkron bir işlem yapması gerektiğinde, her defasında Thread classından yeni bir instance almak.
2. Threadlerin yönetimini uygulamadan soyutlamak
Yani uygulamanın asenkron işlemlerini bir executor'a paslamak. (Java 5.0)

Thread tanımlamak ve çalıştırmak

Bir uygulama Thread nesnesi yaratacaksa, threadin çalıştıracağı kodu da mutlaka belirtmelidir.
Bunu yapmanın iki yolu vardır:

1. Runnable interface

Runnable interface'ini implement edebiliriz.

Runnable interface'i tek bir run() metodu tanımlar. Yaratılan runnable implementasyonu bir Thread'in constructor'ında argüman olarak verilir.

Runnable.java'yı açıp baktığımızda çok basit olduğunu görüyoruz (commentlerden arındırılmıştır):
...
package java.lang;

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

...

Runnable.java dosyasındaki commentleri okuduğumuzda bu interface'in yaratılmasındaki amacı anlayabiliriz.
Burada özellikle sadece run() metodu çalıştırılacaksa, Thread classını extend etmek yerine bu interface'i kullanmamız öneriliyor.
Çünkü bir classın davranışını değiştirmek ya da geliştirmek gibi bir niyetiniz yoksa, o classı extend etmeniz yanlış olur.
Eğer ki sadece bir Thread instance'ın run() metodunu kullanmamız gerekiyorsa, o zaman bütün bir Thread classını extend etmeye gerek yoktur.
Özellikle ileride başka bir classı extend etmek gerektiğinde de engel teşkil etmemesi açısından, daima interface'e göre kodlama yapmak ("program to an interface, not an implementation" prensibi) doğru bir yaklaşım olacaktır.

Örnek:

public class HelloRunnable implements Runnable {

    public void run() {
        System.out.println("Hello from a thread!");
    }

    public static void main(String args[]) {
        (new Thread(new HelloRunnable())).start();
    }

}

...

2. Thread class

Thread classından kalıtım alabiliriz.

Thread classı Runnable interface'ini implement eder. Fakat run() metodu boştur.
Bir uygulama Thread'i extend ederek bir alt sınıf yaratıp, kendi run() metodu implementasyonunu yapabilir.

Thread.java dosyasını açıp baktığımızda oldukça uzun bir dosya olduğunu görüyoruz. Bir kısmını gözardı edersek özeti şu şekilde kalır:
...
public class Thread implements Runnable {
   
    public final static int MIN_PRIORITY = 1;
    public final static int NORM_PRIORITY = 5;
    public final static int MAX_PRIORITY = 10;

    public static void sleep(long millis, int nanos)   throws InterruptedException {    }
 
    public Thread() {  init(null, null, "Thread-" + nextThreadNum(), 0);    }
    public Thread(Runnable target) {  init(null, target, "Thread-" + nextThreadNum(), 0);    }

    public synchronized void start() {   }   
    @Override
    public void run() { if (target != null) { target.run(); }}     
 
    @Deprecated
    public final void stop() {    }   
    @Deprecated
    public final synchronized void stop(Throwable obj) { }
 
    public void interrupt() { }
 
    @Deprecated
    public void destroy() {  }    
    @Deprecated
    public final void suspend() {    }   
    @Deprecated
    public final void resume() {    }
   
    public final synchronized void join(long millis)    throws InterruptedException { }  
    public final void join() throws InterruptedException {        join(0);    }

    public static void dumpStack() {        new Exception("Stack trace").printStackTrace();    }

    public final void setDaemon(boolean on) {  }  
    public final boolean isDaemon() {  }  

    public final void checkAccess() {  }
    public static native boolean holdsLock(Object obj);    
    public enum State {  
        NEW,    
        RUNNABLE,    
        BLOCKED,    
        WAITING,      
        TIMED_WAITING,    
        TERMINATED;
    }      
}

...
Bütün bunların arasında önemli noktalar şunlardır:

* Thread classı Runnable interface'ini implement eder. run() metodunu override eder.

* Yeni thread instance yaratılırken runnable nesnesi parametre olarak verilebilir.

* start() metodu ile çalıştırılırlar ve bu metod synchronized modifier'ı ile işaretlenmiştir

* sleep() metodu ile milisaniye (saniyenin binde biri) cinsinden bekletilirler

* thread'lerin priority yani öncelikleri vardır.

* Ayrıca Thread'lerin state'leri vardır:
  public enum State {  
        NEW,    
        RUNNABLE,    
        BLOCKED,    
        WAITING,      
        TIMED_WAITING,    
        TERMINATED;
    }

* stop(), destroy(), suspend(), resume() metodları Deprecated yani kullanımdan kalkmıştır.


Kısacası Thread sınıfını extend ederek run metoduna kendi implementasyonumuzu yazdığımız zaman, artık yeni aldığımız instance'ları start() metodu ile çalıştırabiliriz.

Örnek:
public class HelloThread extends Thread {

    public void run() {
        System.out.println("Hello from a thread!");
    }

    public static void main(String args[]) {
        (new HelloThread()).start();
    }

}
...
Hangisini tercih etmeli?

Bu iki yaklaşımdan birincisi, yani Runnable interface'ini implement etmek hem daha esnek bir yaklasım oluyor, hem de high-level thread management API'de de kullanılabiliyor.
İkinci yöntem daha basit kullanımı olmasına rağmen, gereksiz yere subclassing yapıldığından genelde önerilmiyor.

Thread classı thread yönetimi için bir çok faydalı metod sağlıyor. Statik metodlar metodu çağıran thread hakkında bilgi verebiliyor.

sleep metodu ile çalışmayı duraklatmak


Thread.sleep() metodu threadin çalışmasını belirli bir süre boyunca duraklatır.
Bu sayede o anda çalışmakta olan diğer threadler de işlemciden yararlanabilirler.
sleep() metodu aynı zamanda threade zaman aralıkları verilmesini ve bir threadin diğerinin işlemini bitirmesini beklemesini sağlar.

sleep() metodunun iki tane overloaded versiyonu mevcuttur: biri milisaniye, diğer ise nanosaniye cinsinden bekleme süresini parametre alır.
Fakat bu sürelerin kesin olmayacağını unutmamak gerekiyor. Çünkü bu zamanlamaların ayarlanması alttaki işletim sistemine bağlıdır ve interruptlar ile de zamanlamanın bölünmesi mümkündür.
Kısacası sleep metodunun threadi kesin olarak verilen miktarda bekleteceğinin garantisi yoktur.

Örneğin aşağıdaki program Thread.sleep() metodundan yararlanarak 4 saniyelik aralıklarla ekrana yeni bir mesaj yazıyor:

public class SleepMessages {
    public static void main(String args[]) throws InterruptedException {
        String info[] = {"Mares","Does","Lambs","ivy"};

        for (int i = 0; i < importantInfo.length; i++) {            
            Thread.sleep(4000);            
            System.out.println(info[i]);
        }
    }
}
...

Buradaki InterruptedException, eğer bu thread sleep durumunda iken başka thread tarafından interrupt edilirse ortaya çıkar.
Bu örnekte bu threadi interrupt edebilecek başka bir thread tanımlanmadığından, exceptionı yakalamak için bir şey yazmaya gerek yok.

Interrupts


Interrupt bir threadin yaptığı işi bırakıp başka bir şeye geçmesi gerektiğinin işaret edilmesidir.
Genelde interrupt edilen thread terminate eder (fakat başka davranışlar verdirilmesi de mümkündür).
Bir thread diğerini interrupt edebilmek için o instance'ın interrupt() metodunu çağırır.
interrupt mekanizmasının doğru çalışması için interrupt edilen threadin kendi interruptını desteklemesi gereklidir.

Bir threadin kendi interrupt'ını desteklemesi
Genel davranış, interrupt geldiğinde run() metodundan hemen return etmektir.
Örneğin SleepMessages classına interrupt desteği vermek için exceptionı yakaladığımız yere return ifadesini eklememiz yeterlidir:

for (int i = 0; i < importantInfo.length; i++) {
    // Pause for 4 seconds
    try {
        Thread.sleep(4000);
    } catch (InterruptedException e) {
        // We've been interrupted: no more messages.
        return;
    }
    // Print a message
    System.out.println(importantInfo[i]);
}
...
sleep() gibi InterruptedException veren metodlar genellikle hemen yaptığı işi iptal edip return etmek üzere tasarlanmıştır.
Örnek:

for (int i = 0; i < inputs.length; i++) {
    heavyCrunch(inputs[i]);
    if (Thread.interrupted()) {
        // We've been interrupted: no more crunching.
        return;
    }
}
...
Eğer kodun uzun bir kısmında InterruptedException veren bir metod yoksa, bu durumda düzenli olarak interrupt gelip gelmediğini kontrol etmesi gerekir. Bunun için boolean döndüren  Thread.interrupted() metodunu kullanabiliriz:

for (int i = 0; i < inputs.length; i++) {
    heavyCrunch(inputs[i]);
    if (Thread.interrupted()) {
        // We've been interrupted: no more crunching.
        return;
    }
}
...
Yukarıdaki gibi basit olmayan daha kompleks uygulamalarda ise interrupt alındığı zaman bir exception throw edilmesi mantıklı olur. Böylece exception handling kodları tek bir catch içerisinde toplanabilir:

if (Thread.interrupted()) {
    throw new InterruptedException();
}
...

interrupt status flag

interrupt mekanizması, interrupt status adlı bir flag aracılığıyla gerçekleşir. Thread.interrupt() metoduset edildiğinde interrupt edilmiş olur. Bir thread, Thread.interrupted() statik metodu ile interrupt edilip edilmediğini kontrol ettiğinde interrupt status sıfırlanır.
Statik olmayan isInterrupted() metodu ile başka bir threadin interrupt durumunu öğrenebiliriz. Bu metod interrupt status flagine dokunmaz.

Genellikle InterruptedException throw eden bir metod return ettiği zaman interrupt status flagini de sıfırlar. Fakat bunun hemen arkasından başka bir threadin interrupt metodunu çağırmasıyla interrupt status flaginin tekrar set edilmesi mümkündür.

Joins


Join bir threadin diğerinin işini bitirmesini beklemesi anlamına gelir.
Bir t threadi o anda çalışmakta ise bulunduğumuz yere t.join() yazarsak current thread pause moduna geçerek, t'nin bitmesini bekleyecektir.
join() metodunun overloaded versiyonları ile belirli zaman aralıkları verebiliriz.
Yalnız sleep() metodunda olduğu gibi join() metodunda da kesin olarak verilen süre kadar bekleneceğinin garantisi yoktur.

sleep() metodu gibi join() metodu da interrupt edildiğinde InterruptedException fırlatarak exit eder.

Örnek:
SimpleThreads classında iki thread bulunuyor. Eğer MessageLoop threadi gereğinden uzun sürerse, main thread onu interrupt ediyor. Interrupt edilen thread ekrana henüz işinin bitmediği bilgisini yazıp exit ediyor. Bu esnada main thread onun yokolmasını join() ile bekliyor.

public class SimpleThreads {

    // Display a message, preceded by
    // the name of the current thread
    static void threadMessage(String message) {
        String threadName =
            Thread.currentThread().getName();
        System.out.format("%s: %s%n",
                          threadName,
                          message);
    }

    private static class MessageLoop
        implements Runnable {
        public void run() {
            String importantInfo[] = {
                "Mares eat oats",
                "Does eat oats",
                "Little lambs eat ivy",
                "A kid will eat ivy too"
            };
            try {
                for (int i = 0;
                     i < importantInfo.length;
                     i++) {
                    // Pause for 4 seconds
                    Thread.sleep(4000);
                    // Print a message
                    threadMessage(importantInfo[i]);
                }
            } catch (InterruptedException e) {
                threadMessage("I wasn't done!");
            }
        }
    }

    public static void main(String args[])
        throws InterruptedException {

        // Delay, in milliseconds before
        // we interrupt MessageLoop
        // thread (default one hour).
        long patience = 1000 * 60 * 60;

        // If command line argument
        // present, gives patience
        // in seconds.
        if (args.length > 0) {
            try {
                patience = Long.parseLong(args[0]) * 1000;
            } catch (NumberFormatException e) {
                System.err.println("Argument must be an integer.");
                System.exit(1);
            }
        }

        threadMessage("Starting MessageLoop thread");
        long startTime = System.currentTimeMillis();
        Thread t = new Thread(new MessageLoop());
        t.start();

        threadMessage("Waiting for MessageLoop thread to finish");
        // loop until MessageLoop
        // thread exits
        while (t.isAlive()) {
            threadMessage("Still waiting...");
            // Wait maximum of 1 second
            // for MessageLoop thread
            // to finish.
            t.join(1000);
            if (((System.currentTimeMillis() - startTime) > patience)
                  && t.isAlive()) {
                threadMessage("Tired of waiting!");
                t.interrupt();
                // Shouldn't be long now
                // -- wait indefinitely
                t.join();
            }
        }
        threadMessage("Finally!");
    }
}
...

Synchronization


Threadler yukarıdaki örnekte kaynakları ortaklaşa kullanarak iletişim kuruyorlar. Aynı primitif ya da nesnelere erişebiliyorlar.
Bu şekilde yapılan iletişim çok etkili olsa da, thread interferance ve memory consistency hatalarına sebep olabiliyor.
Bu hatalardan korunmak için synchronization yapılması gerekiyor.
Yalnız synchronization yapıldığında da thread contention yani birden fazla threadin aynı kaynağa aynı anda erişmeye çalışılması problemi ortaya çıkıyor.
Bu durumda java runtime bazı threadleri yavaşlatıp bazen tamamen durdurabiliyor.
Thread contention örnekleri olarak starvation ve livelock verilebilir.

Thread interferance


Bazen bir kaynağa aynı anda erişen birden fazla thread işlem yaparsa farklı sonuçlar oluşur.
Bu durumda threadlerden biri diğerlerinin elindeki sonucun üzerine yazar ve sonuçlarsa tutarsızlık ortaya çıkar. Buna thread interferance yani threadlerin birbirine karışması denir.

Örneğin aşağıdaki counter sınıfından bir instance alınır ve bu instance üzerinde birden fazla thread aynı anda işlem yaparsa, sonuçlar beklenenden farklı çıkabilir.

class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}
...
Çünkü ++ ve -- işlemleri göründüğünün aksine atomik işlemler değildir. Yani birden fazla işlemden oluşur.
Örneğin c++ işlemi aslında
1) c'nin okunması
2) alınan değere bir eklenmesi
3) bulunan sonucun tekrar c'ye yazılması
şeklinde 3 aşamadan oluşur.

Böyle durumların olup olmayacağı önceden tahmin edilemediğinden, bazen de doğru sonuçlar verdiği için farkedilmesi çok zordur.

Memory consistency errors


Memory consistency hatası, birden fazla threadin aynı görmeleri gereken bir değeri farklı görmeleridir.
Bir işlemin mutlaka diğeri başlamadan önce bitmesi gerektiğini ifade etmek için happens-before relationship kavramı kullanılır. Happens-before ilişkisi kurulmadan yapılan işlemler, birbirinden habersiz olduklarından tutarsız sonuçlara neden olur.

Thread.start() ve Thread.join() metodları happens-before ilişkileri kurabilirler.
Aynı şekilde synchronized ve volatile keywordleri ile de happens-before ilişkileri kurulabilir.

Synchronized methods


Java'da senkronlama işlemi için synchronized method ve synchronized statement'lar kullanılır.

Bir metodu senkron hale getirmek için declaration'ın başında synchronized keywordu koymak yeterlidir.

Örnek:

public class SynchronizedCounter {
    private int c = 0;

    public synchronized void increment() {
        c++;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }
}
...
Buradaki metodları senkron hale getirdiğimizde şunları sağlamış oluruz:
1- Aynı instance'ın aynı metodunun birden fazla thread tarafından aynı anda çağırılması imkansızdır.
Yani bir thread o nesnenin senkron bir metodunu çağırdığında, aynı nesnenin aynı metodunu çağıran tüm diğer threadler birinci threadin işi bitene kadar bloklanır, yani çalışmayı durdururlar.

2- Bir senkron metodun işi bittiğinde hemen sonraki metod çağrıları ile arasında otomatik olarak happens-before ilişkisi kurulur. Yani nesnenin durumunda (state) yapılan değişiklikler anında bu metodu çağıran diğer threadlere de görünür.

Not: Constructor'lar senkron yapılamaz. Eğer bir constructor'a synchronized keywodu eklenirse compiler hata verir. Bunun sebebi bir nesneyi yaratan constructora sadece onu çağıran threadin  erişebilir olmasıdır. Yani senkronlamaya gerek yoktur.

Uyarı: bir classın bütün instance'larını bir listede tutmak isteyebiliriz. Fakat bunu yaparken nesnenin yaratılması bitmeden referansının diğer threadler tarafından çağırılmamasına dikkat etmek gerekir.
Örneğin
instances.add(this);
...
Yukarıdaki satırı constructor'a eklersek, diğer threadler instances adlı liste aracılığıyla daha yaratılmadan bu nesneye erişmeye çalışabilir.

Senkron metodlar sayesinde thread interferance ve memory inconsistency hatalarına karşı gereken önlemler alınmış olur. Eğer bir nesne birden fazla threade görünüyorsa o nesnenin variable'larına yapılan bütün okuma ve yazma işlemleri synchronized metodlar aracılığıyla yapılır.

Not: final keywordu ile işaretlenen field'lar bir kez initialize edildikten sonra asla değiştirilemezler. Bu yüzden senkron olmayan normal metodlarla güvenli şekilde okunabilirler.
Yalnız senkron olması istenen alanlar final ile işaretlenirse, liveness adı verilen bir problem ortaya çıkabilmektedir.

Intrinsic locks and synchronization


Synchronization işlemi, instrinsic lock ya da monitor lock (API spec. de sadece monitor olarak geçer) adı verilen internal bir entity ile sağlanır.
Bu lock yani kilitler bir nesnenin state'ine exclusive (tekil) erişim sağlanması ve happens-before ilişkilerinin kurulmasında hayati önem taşır.

Her nesnenin bir kilidi (lock) vardır. Threadler bir nesnenin field'larına erişmek için önce kilidi ele geçirmek zorundadır. Kilide erişimi elde ettiğinde işi bitene kadar nesneyi kilitler; işi bittiğinde kilidi açar (release).
Bir thread bir nesnenin kilidini ele geçirdiğinde o kilide sahip olduğunu söyleriz (owns the lock). Kilit serbest bırakılana kadar diğer threadler o kilide erişemezler.
Erişmeye çalışan bütün threadler bloklanır.

Kilit serbest bırakıldığında bir sonraki ele geçirme işlemi ile arasında happens-before ilişkisi kurulur.

Locks in synchronized methods

Bir thread bir nesnenin synchronized metodunu çağırdığı anda o nesnenin kilidini ele geçirir. Metod normal şekilde ya da exception ile return ettiği anda kilit açılır.

Not: static bir metod synchronized olduğunda ne olur? static metodlar nesnelere değil classlara bağlıdır. Bu yüzden thread ilgili sınıfa bağlı olan Class nesnesinin kilidini ele geçirir.
Böylece ilgili classın static field'larına instance nesnelerinin kilitlerinden bağımsız olan, ayrı bir kilit aracılığıyla erişilmiş olur.

Synchronized statements

Senkronlamanın bir diğer yöntemi de senkron statement kullanmaktır. Yalnız senkron statement'larda senkronlanacak (yani dışarıdan erişime karşı kilitlenecek) olan nesnenin parantez içinde belirtilmesi gereklidir.

public void addName(String name) {
    synchronized(this) {
        lastName = name;
        nameCount++;
    }
    nameList.add(name);
}
...
Yukarıdaki addName() metodu ile lastName ve nameCount alanlarındaki değişimi senkronize etmek istiyoruz. Fakat aynı zamanda nameList nesnesinin add() metod çağrılarını bu kapsamda senkronlamak istemiyoruz (nesneye erişimi bloklamak istemiyoruz - senkron bir kod bloğu içinde başka nesnelerin metodları çağrılırsa Liveness problemleri ortaya çıkabilir).

Bu durumda addName metodunu synchronized keywordu ile senkron yapıp, içindeki nameList.add için senkron olmayan ayrı bir metod yazmak yerine; tek bir asenkron metod yazıp bu metodda senkronlamak istediğimiz alanları synchronized(this) {} bloğu içerisine hapsetmemiz yeterli oluyor.

Senkron bloklar aynı zamanda daha ince ayar gerektiren durumlarda da çok kullanışlıdır.
Örneğin birbirinden ayrı olarak okunup yazılan c1 ve c2 alanlarını senkronize etmek istiyoruz.
Fakat bunların herhangi birine erişileceği zaman diğerinin de otomatik olarak kilitlenmesini, ve onu kullanan threadlerin bloklanmasını istemiyoruz.
Bu durumda this keywordu ile bütün alanları kilitlemek yerine, bu alanlara özel kilit nesneleri yaratarak özelleştirilmiş kilitler ile senkronlama yaparız.

public class MsLunch {
    private long c1 = 0;
    private long c2 = 0;
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    public void inc1() {
        synchronized(lock1) {
            c1++;
        }
    }

    public void inc2() {
        synchronized(lock2) {
            c2++;
        }
    }
}
...

Yukarıdaki örnekte lock1 ve lock2 adlı nesneler c1 ve c2 alanlarını ayrı ayrı senkronlayan kilit görevi görüyorlar. Bu sayede c1 değişkeni artırılacağı zaman c2 değerini artırmak isteyen bir threadin nesneye erişimi bloklanmıyor.

Not: Bu yöntemi uygularken bu iki alana farklı threadler tarafından aynı anda erişilmesinin hiçbir sakınca oluşturmadığından mutlaka emin olunması gerekiyor.

Reentrant synchronization

Bir thread başka bir threadin elinde bulunan kilide erişemez. Fakat kendi elinde bulunan bir kilide erişebilir. Bir threadin bir kilidi birden fazla kez elde etmesine reentrant synchronization denir.

Bunun olabilmesi için bir threadin senkronize bir kod bloğu içerisinde direk ya da dolaylı olarak yine senkron kod içeren bir metodu çağırması gerekir. Bu durumda iki kod bloğu da aynı kilidi kullanır.

Reentrant synchronization olmasaydı threadlerin kendi kendilerini bloklamaması için ekstra önlemler alınması gerekecekti.

Atomic access


Atomic işlem tek seferde yapılan işlem demektir. Atomic bir işlem yarıda kesilemez; ya tamamen gerçekleşir, ya da hiç gerçekleşmez. Atomic bir işlem bitene kadar sistemde hiç bir etkisi görülmez.

Increment operasyonunun (c++) atomic olmadığını biliyoruz. Bu gibi işlemler tek seferlik gibi görünse de aslında birden fazla işlemin birleşmesinden oluşmuştur.
Atomic işlemlere örnek vermek gerekirse:
1- Bütün referans değişkenleri ve çoğu primitif değişkenin (long ve double hariç hepsi) okunması ve yazılması işlemleri atomiktir.
2- volatile keywordu ile işaretlenen bütün değişkenlerin (long ve double dahil) okunması ve yazılması atomiktir.

Atomik işlemler tek seferde yapıldığından birbirine karışmazlar. Bu yüzden thread interferance problemi yaratmazlar. Fakat yine de memory consistency hataları olabileceğinden senkronlama ihtiyacı vardır.
Volatile keywordunu kullandığımız zaman üzerinde yazma yaptığımız değişkenin bir sonraki okunması işlemi ile happens-before ilişkisi kurulur. Yani volatile olan değişkene yapılan bir değişiklik diğer threadler tarafından yapılan okumalarda görünecektir.
Bu sayede memory consistency hatalarının ortaya çıkma riski büyük oranda düşecektir.

java.util.concurrent paketindeki bazı sınıflarda senkronlamaya gerek duymayan atomik metodlar bulunur.

Liveness


Bir concurrent yani çok threadli uygulamanın belirtilen zamanlamaya uygun çalışmasına liveness denir.
En yaygın liveness problemi deadlock problemidir. Diğer bazı liveness problemleri de starvation ve livelock'tır.

Deadlock


Deadlock birden fazla threadin birbirini bekleyerek sonsuza kadar bloklanmaları durumuna denir.

Örneğin bir kişinin diğerine selam vermek için eğildiği, ve nezaket icabı diğeri selamını alana kadar doğrulmadığı bir kültürü varsayalım. Bu kültürde iki kişinin aynı anda selam vermeleri halinde, ikisi de bir diğerinin selamını almasını sonsuza kadar bekleyecektir.
Kimse diğerinin selamını alamadığından doğrulamayacak, ve doğrulamadığı için de yine selam alamayacaktır. Durum böylece bir nevi kısırdöngüye yani deadlock'a dönüşecektir.

Örnek:

public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void selamVer(Friend dost) {
            System.out.format("%s: %s" + "  bana selam verdi!%n", this.name, dost.getName());
            dost.selamAl(this);
        }
        public synchronized void selamAl(Friend dost) {
            System.out.format("%s: %s" + " selamımı aldı!%n", this.name, dost.getName());
        }
    }

    public static void main(String[] args) {
        final Friend ali = new Friend("Ali");
        final Friend veli = new Friend("Veli");
        new Thread(new Runnable() {
            public void run() { ali.selamVer(veli); }
        }).start();
        new Thread(new Runnable() {
            public void run() { veli.selamVer(ali); }
        }).start();
    }
}
...
Burada ali, veli'ye selam verdikten sonra veli'nin selamAl() metoduna erişmeye çalışıyor.
Fakat ali'nin veli'nin selamAl() metoduna erişmesi imkansız; çünkü veli nesnesi o esnada kendi selamVer() metodu içerisinde ali'nin selamAl() metoduna erişmeyi bekliyor.
Böylece ali veli'ye, veli de ali'ye kendi selamVer() metodları içinde bloklanmış vaziyette beklemede kaldıklarından erişemiyorlar. Ve deadlock oluşuyor.

Starvation and Livelock


Deadlock kadar sık görülmese de, concurrent yazılımla uğraşanların zaman zaman karşılaştığı problemlerdir.

Starvation

Starvation, bir threadin bir kaynağı elinden çok uzun süre bırakmaması yüzünden diğer threadlerin o kaynağa erişmeyi bekleyerek bloklanıp kalmasıdır.
Genellikle açgözlü threadler bu probleme sebep olurlar. Örneğin bir nesnenin senkronize bir metodunun bitmesi çok uzun sürerse, bu metodu sıklıkla çağıran bir metod diğerlerinin erişimini büyük ölçüde engelleyecektir.

Livelock

Bir thread genellikle başka bir threadin davranışına cevap olarak harekete geçer.
Eğer bu davranışı yapan thread de bir diğer thread'in davranışına cevap olarak çalıştıysa, livelock ortaya çıkabilir.
Deadlock'ta olduğu gibi livelock'ta da threadler ilerleme kaydedemezler.
Yalnız burada threadler bloklanmamıştır. Diğer threadlere cevap vermekten o kadar meşguldür ki kendi işini yapmaya zaman ayıramaz.

Livelock problemi bir koridorda birbiriyle karşılaşıp bir türlü diğerinin yanından geçemeyen iki kişinin durumuna benzer.
Örneğin Ali ile Veli karşılaşırlar. Ali, Veli'nin yanından geçebilmek için sağa adım atar. Veli de Ali'nin yanından geçebilmek için sola adım atar. İkisi de yine karşı karşıya olduklarından diğer tarafa birer adım atarlar. Fakat yine karşı karşıya kaldıklarından geçemezler..


Guarded blocks


Bir thread diğerinin yapacağı bir eylemin sonucunu bekliyorsa bunu sağlamak için boş bir while döngüsü koymak boş yere işlemcinin zamanından çalınmasına neden olur.
Örnek:

public void guardedJoy() {
    // Simple loop guard. Wastes
    // processor time. Don't do this!
    while(!joy) {}
    System.out.println("Joy has been achieved!");
}
...

Bunun yerine Object.wait() metodunu kullanarak threadi bekletirsek, işlemcinin zamanından çalmamış oluruz. Buradaki wait() metodu diğer bir threadden herhangi bir notification alana kadar bekleyecektir.
Örnek:

public synchronized void guardedJoy() {
    // This guard only loops once for each special event, which may not
    // be the event we're waiting for.
    while(!joy) {
        try {
            wait();
        } catch (InterruptedException e) {}
    }
    System.out.println("Joy and efficiency have been achieved!");
}
...
wait() metodu çağırıldığında thread elindeki kilidi bırakacak ve beklemeye geçecektir.
Daha sonra diğer bir thread o nesnenin kilidini ele geçirip notifyAll() metodunu çağırırsa, bu nesne üzerindeki kilidi bekleyen bütün threadlere notification gönderilecektir.
Bildirimi yapan bu thread artık kilidi bırakacaktır.
Birinci thread bu notification'ı aldıktan sonra, ikincinin bıraktığı kilidi tekrar ele geçirdiği anda, wait işlemini bitirerek kaldığı yerden çalışmasına devam edecektir.


Hiç yorum yok:

Yorum Gönder