İçeriğe geç
academia.sh

Ders 05 / 11

İş Parçacıkları ve Yaşam Döngüsü

Aynı gövde run() ile çağıranda, start() ile oluşturulan bir iş parçacığında koşuyor; fark tek bir yöntem adı. Üç kesme kurulumundan yalnız biri gövdeyi durduruyor, kalan ikisi kesme çağrısı olsun olmasın aynı adıma ulaşıyor.

İçindekiler

Anında Derleme dersi şu sonuçla bitti: sınıf dosyasında bulunmayan bir optimizasyon kararını veren taraf programın kendisi değildi, ama kodu koşturan iş parçacıklarını oluşturan yine programdı. Bu ders oradan başlıyor. Sınıf dosyasını kim yüklüyor, kararı kim veriyor, profili kim üretiyor sorularının hepsi bir gövdenin zaten koştuğunu varsayıyordu. Şimdi bir adım geri gidiliyor: bir iş parçacığı nasıl var ediliyor, ve az önce yazılmış olan gövdesini hangi taraf koşturuyor?

Sorunun yanıtı çağrı yazımında görünmüyor. Runnable gövdesi tek bir yerde tanımlanır ve iki ayrı yöntemle tetiklenebilir; ikisi de aynı satırı çalıştırır, ikisi de derleyici için geçerli bir çağrıdır. Bu dersin ölçtüğü ilk fark — kursun ortak çekirdeğinin de ilk iki satırı — tam olarak burada duruyor.

Kursun sorduğu soru iki parçadan oluşuyor: bir adımı hangi iş parçacığı koşturuyor, ve o iş parçacığını kim var etti? Bu ders yalnız ikinci parçaya, var etmenin kendisine bakıyor. Bir program çalışırken zaten en az bir iş parçacığı vardır — main yöntemini koşturan iş parçacığı — ve program başka hiçbir iş parçacığı istemezse bu tek başına yeterlidir. Yeni bir iş parçacığı istemek gövdeyi ayrı bir akışa taşır; o akış artık çağıranın akışı değildir ve kendi başına başlar, kendi başına biter. Aşağıdaki iki bölüm bu var etme ve bitirme sürecini, ortadaki üçüncü bir soruyla birlikte ölçüyor: yarım kalmış bir iş parçacığına “artık dur” demenin gövdeye ne yaptırdığı.

Aynı Gövde, İki Koşum Yolu

  • EZ1 — Kim koşturduğunu söyleyen etiket, iş parçacığının kendisinden değil, ayrı tutulan bir kayıttan okunur; bir iş parçacığı Kosum.tanit ile kaydolmadıysa bilinmeyen sayılır.
  • EZ2Govdeler.main’i çalıştıran iş parçacığı baştan "cagiran" diye kaydedilir; bu dersin geri kalanında “çağıran” hep bu iş parçacığıdır.
// Kosum.java — bir govdeyi hangi is parcaciginin kosturdugunu etiketle soyler
import java.util.*;

public class Kosum {
    static final Map<Thread, String> etiketler =
            Collections.synchronizedMap(new IdentityHashMap<>());

    static void tanit(String etiket) { etiketler.put(Thread.currentThread(), etiket); }

    static String kim() {
        String e = etiketler.get(Thread.currentThread());
        return e == null ? "bilinmeyen" : e;
    }

    static void gozle(String nokta) {
        System.out.printf("%-32s %s%n", nokta, kim());
    }
}

Kosum, iş parçacığının adını, kimlik karmasını ya da önceliğini hiç okumaz — bunların hepsi ortama bağlı veridir. Tek okuduğu şey, tanit ile önceden yazılmış bir etikettir; kimlik sorusu böylece bizim verdiğimiz ada indirgenir.

// Govdeler.java — ayni govde run() ile cagiranda, start() ile olusturulan is parcacikinda kosuyor
public class Govdeler {
    public static void main(String[] args) throws InterruptedException {
        Kosum.tanit("cagiran");
        Runnable govde = () -> Kosum.gozle("govde");

        // 1. dogrudan cagri: run() adi baska bir is parcacigi baglamaz, cagiran kosturur
        new Thread(govde).run();

        // 2. start(): govdeyi olusturulan is parcacigi kosturur
        Thread t = new Thread(() -> { Kosum.tanit("olusturulan"); govde.run(); });
        t.start();
        t.join();
    }
}
govde                            cagiran
govde                            olusturulan

İki satır da aynı govde referansını çalıştırıyor, ikisi de Runnable.run() çağırıyor. Fark kimin run() dediğidir: birinci satırda new Thread(govde) nesnesinin run()’ı doğrudan çağrılıyor — bu, sıradan bir yöntem çağrısıdır, yeni bir iş parçacığı oluşturmaz, sadece gövdeyi çağıranın kendi yığıtında çalıştırır. İkinci satırda start() çağrılıyor; bu, çalışma zamanına yeni bir iş parçacığı isteği yollar ve gövde o iş parçacığında koşar. t.join() olmasaydı main’in gövdenin bitişini beklediğini hiçbir şey garanti etmezdi — bu köprü aşağıdaki sınırlayıcı ölçümde ayrıca ele alınıyor.

new Thread(govde) ifadesinin kendisi, henüz hiçbir şey koşturmuyor. İki koşum yolunda da önce bir Thread nesnesi kuruluyor; nesne kurulduğu an yalnızca bir kayıt taşıyor, gövdeyi kimin çalıştıracağına dair henüz bir taahhüt yok. Birinci satırda o nesne hiç start() edilmeden run() ile çağrılıyor — nesne var ama iş parçacığı hiç talep edilmemiş oluyor, gövde doğrudan çağıranın kod akışına giriyor. İkinci satırda ise start() çağrısı bu taahhüdü veriyor: çalışma zamanından yeni bir iş parçacığı istiyor, ve gövde artık o isteğin karşılığı olan iş parçacığında koşuyor. Aradaki fark, çağrının kendisinin bir iş parçacığı var ettiği ile var etmediği arasındaki farktır — ve bu fark, iki satırın ikisinde de aynı görünen run() çağrısının arkasında saklı kalıyor.

Yaşam Döngüsü: Belirlenimci Gözlemler

  • EZ3isAlive() iş parçacığının kendisinden hiçbir kimlik verisi okumaz, yalnız “başlatıldı ve henüz bitmedi mi” sorusuna true/false döner; bu, ortama bağlı değildir.

Bir Thread nesnesi start() çağrılmadan önce yenidir, start() sonrası ve bitmeden önce çalışır durumdadır, join()’in döndüğü an ise kesin olarak bitmiştir. Üç durum da isAlive() ile belirlenimci biçimde gözlenir; aradaki geçişler zamanlamaya değil, hangi yöntemin çağrıldığına bağlıdır. isAlive()’ın kendisi de iş parçacığından hiçbir ortama bağlı veri okumuyor — döndürdüğü tek bilgi bir mantıksal değer, ve o değer hangi yöntemlerin daha önce çağrıldığından belirleniyor, makinenin o an ne kadar meşgul olduğundan değil.

// YasamDongusu.java — durumlar belirlenimci gozlenir: baslamadan once, calisirken, bittikten sonra
import java.util.concurrent.CountDownLatch;

public class YasamDongusu {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch baslasin = new CountDownLatch(1);

        Thread t = new Thread(() -> {
            try { baslasin.await(); } catch (InterruptedException e) { }
        });

        System.out.println("baslamadan once  isAlive: " + t.isAlive());
        t.start();
        System.out.println("start sonrasi    isAlive: " + t.isAlive());
        baslasin.countDown();
        t.join();
        System.out.println("join sonrasi     isAlive: " + t.isAlive());

        try {
            t.start();
            System.out.println("ikinci start     : basarili");
        } catch (IllegalThreadStateException e) {
            System.out.println("ikinci start     : " + e.getClass().getSimpleName());
        }
    }
}
baslamadan once  isAlive: false
start sonrasi    isAlive: true
join sonrasi     isAlive: false
ikinci start     : IllegalThreadStateException

baslasin gövdeyi bir CountDownLatch üzerinde bekletiyor; böylece start() döndükten sonra gövdenin henüz bitmediği, isAlive() ile koşuma bırakmadan garanti ediliyor. join() çağrısı iki işi birden yapıyor: hem çağıranı gövde bitene kadar durduruyor hem de dönüşünden sonra isAlive()’ın false olacağını kesinleştiriyor — join, bu dersin sıralama aracıdır. Son satır ayrı bir kuralı gösteriyor: bir Thread nesnesi yalnız bir kez başlatılabilir; ikinci start() çağrısı, nesnenin durumundan bağımsız olarak her zaman IllegalThreadStateException fırlatır.

Bu kısıtlamanın nedeni yaşam döngüsünün kendisidir. start(), bir Thread nesnesini yeni durumdan çıkarıp çalışma zamanına bir kez teslim eder; teslimden sonra o nesne bir daha hiç yeni duruma dönmez, bitince kalıcı olarak sona ermiş sayılır. Bir nesneyi ikinci kez başlatmaya izin vermek, aynı iş parçacığı kimliğinin iki ayrı koşum boyunca yeniden kullanılması demek olurdu; çerçeve bunun yerine her koşum için taze bir Thread nesnesi kurulmasını zorunlu kılar. Bitmiş bir gövdeyi yeniden çalıştırmak isteyen kod, aynı Runnable gövdesini yeni bir Thread nesnesine sararak yapar — nesne değişir, gövde değişmez.

Kesme: Bir İstek, Cevap Gövdede

  • EZ4 — Kesme bir buyruk değildir; Thread.interrupt() yalnız bir bayrağı işaretler ya da engelleyen bir çağrıda istisna düşürür. Gövdenin bu isteğe ne yapacağı, gövdenin kendi kodunda yazılıdır — çerçevenin belirlediği bir şey değildir.

Üç kurulum aynı deseni paylaşıyor: bir iş parçacığı başlar, kendini CountDownLatch ile kaydettirir, sonra kesme isteği (isteğe bağlı olarak) gönderilir, en son join() ile tamamlanması beklenir. Değişen tek şey gövdenin kesmeye nasıl davrandığıdır.

Birinci gövdenin döngü koşulu Thread.currentThread().isInterrupted() çağırıyor; bu, bir örnek yöntemidir ve bayrağı okuduktan sonra temizlemez. Aynı soruyu soran ama bayrağı okur okumaz sıfırlayan statik bir karşılığı da var; o karşılık burada kasıtlı olarak kullanılmıyor, çünkü döngüden çıktıktan sonraki sonuc.append satırı hâlâ “bu iş parçacığı kesme istedi” bilgisine ihtiyaç duyabilirdi — sıfırlayan biçim bayrağı ilk okumada silip bu bilgiyi döngünün dışına taşımazdı. Hangi biçimin kullanılacağı da gövdenin kararıdır — çerçeve ikisini de sunar, sonrasında bayrağın gerekip gerekmediğine göre seçim programcıya kalır.

// Kesme.java — kesme bir istektir: govde onu gorebilir ama durup durmamaya kendi kodu karar verir
import java.util.concurrent.CountDownLatch;

public class Kesme {
    static String bayragiDenetleyerekDur(boolean kesEt) throws InterruptedException {
        CountDownLatch basladi = new CountDownLatch(1);
        StringBuilder sonuc = new StringBuilder();
        Thread t = new Thread(() -> {
            basladi.countDown();
            while (!Thread.currentThread().isInterrupted()) { }
            sonuc.append("bayrak gorulunce dongudan cikti");
        });
        t.start();
        basladi.await();
        if (kesEt) t.interrupt();
        t.join();
        return sonuc.toString();
    }

    static String uykudaYakalar(boolean kesEt) throws InterruptedException {
        CountDownLatch basladi = new CountDownLatch(1);
        StringBuilder sonuc = new StringBuilder();
        Thread t = new Thread(() -> {
            basladi.countDown();
            try { Thread.sleep(80); sonuc.append("uyku tamamlandi, "); }
            catch (InterruptedException e) { sonuc.append("istisna yakalandi, "); }
            sonuc.append("ayni sonraki adima gecti");
        });
        t.start();
        basladi.await();
        if (kesEt) t.interrupt();
        t.join();
        return sonuc.toString();
    }

    static String bayragiHicSormaz(boolean kesEt) throws InterruptedException {
        CountDownLatch basladi = new CountDownLatch(1);
        StringBuilder sonuc = new StringBuilder();
        Thread t = new Thread(() -> {
            basladi.countDown();
            long toplam = 0;
            for (long i = 0; i < 20_000_000L; i++) toplam += i;
            sonuc.append("dongu tamamlandi (toplam sifirdan buyuk: " + (toplam > 0) + ")");
        });
        t.start();
        basladi.await();
        if (kesEt) t.interrupt();
        t.join();
        return sonuc.toString();
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("1. bayragi denetleyen govde, kesme cagrisiyla:");
        System.out.println("   " + bayragiDenetleyerekDur(true));
        System.out.println();

        System.out.println("2. uykuda yakalayan govde, kesme cagrisiyla:");
        System.out.println("   " + uykudaYakalar(true));
        System.out.println("2. ayni govde, kesme cagrisi OLMADAN:");
        System.out.println("   " + uykudaYakalar(false));
        System.out.println();

        System.out.println("3. bayragi hic sormayan govde, kesme cagrisiyla:");
        System.out.println("   " + bayragiHicSormaz(true));
        System.out.println("3. ayni govde, kesme cagrisi OLMADAN:");
        System.out.println("   " + bayragiHicSormaz(false));
    }
}
1. bayragi denetleyen govde, kesme cagrisiyla:
   bayrak gorulunce dongudan cikti

2. uykuda yakalayan govde, kesme cagrisiyla:
   istisna yakalandi, ayni sonraki adima gecti
2. ayni govde, kesme cagrisi OLMADAN:
   uyku tamamlandi, ayni sonraki adima gecti

3. bayragi hic sormayan govde, kesme cagrisiyla:
   dongu tamamlandi (toplam sifirdan buyuk: true)
3. ayni govde, kesme cagrisi OLMADAN:
   dongu tamamlandi (toplam sifirdan buyuk: true)

Birinci kurulumun döngüsü yalnız kesme bayrağına bakıyor; kesme çağrısı gelmeseydi bu döngünün başka hiçbir çıkış yolu yoktu — kodun kendisi bunu gösteriyor, bu yüzden o kurulum kesmesiz çalıştırılmıyor. İkinci ve üçüncü kurulumlar ise iki kez, biri kesme çağrısıyla biri çağrısız, koşturuldu ve ikisinde de aynı sonuca ulaştı: Thread.sleep içindeki yakalama bloğu istisnayı yutup bir sonraki satıra geçiyor, sayaç döngüsü ise bayrağı hiç okumadığı için kesmenin varlığından habersiz tamamlanıyor. Üç kurulumun yalnız biri, kesme isteği gelmeden gövdesini hiç bitiremiyor; kalan ikisi kesme isteğini görsün ya da görmesin aynı adıma ulaşıyor. Kesmenin kendisi bir davranış dayatmıyor — davranışı gövdenin yazarı seçiyor.

Aradaki ayrım tek tek bu üç örneğe özgü değil, genelleşen bir kuraldır. Standart kitaplığın engelleyen çağrıları — Thread.sleep, nesne kilidinde beklerken çağrılan bekleme yöntemi, join, eşzamanlılık paketindeki bekleyen çağrılar — kesme bayrağını kendileri denetler ve bayrak işaretliyken (ya da işaretlendiği an) InterruptedException fırlatıp bayrağı temizler. Bunun dışındaki her kod — bir hesaplama döngüsü, bir dosyaya yazma, bir ağ soketi okuma — bayrağı kendiliğinden denetlemez; denetlemek isteyen gövde bunu birinci kurulumdaki gibi kendi elleriyle yazmak zorundadır. İkinci kurulum bu ayrımın ortasında duruyor: sleep bayrağı görüyor ve istisna fırlatıyor, ama gövde o istisnayı yakalayıp hiçbir şey yapmadan yoluna devam ediyor. İstisnanın fırlatılması bir garantidir; ona cevap vermek gövdenin sorumluluğudur ve bu sorumluluk çoğu zaman boş bir catch bloğunda kayboluyor.

Sınırlayıcı Ölçüm: start()’ın Dönmesi Sıra Vermez

Govdeler.main’deki t.start() çağrısı döndüğünde, gövdenin ilk satırının çalışmış olduğunu hiçbir şey garanti etmez. start()’ın görevi yeni bir iş parçacığı istemektir; o isteğin ne zaman yerine getirileceği çağıranın bilgisi dışındadır. Bu yüzden Govdeler.main içinde t.join() çağrılmadan önce gövdenin çıktısını okumaya çalışan bir satır eklenseydi, o satırın gövdenin çıktısından önce mi sonra mı çalışacağı kurguya bağlanmamış bir gözlem olurdu ve ölçüm sayılmazdı — tıpkı bu dersin ortak tanımının kör noktasında yazılana benzer biçimde, “olmadı” ile “sırası kurulmadı” birbirine karıştırılmış olurdu. YasamDongusu ve Kesme derslerindeki her kurulum bu yüzden bir CountDownLatch ile açılıyor: gövde önce kendini kaydettiriyor, çağıran ancak o kayıttan sonra bir sonraki adıma geçiyor. Sıra çizelgeleyiciye bırakılmıyor; await, join ve kayıt noktalarıyla elle kuruluyor.

Bu sınır, ölçümün neden hep aynı iskeleti taşıdığını da açıklıyor. Kesme.bayragiDenetleyerekDur gövdesi kendini basladi.countDown() ile kaydettirmeden önce ana iş parçacığı basladi.await() üzerinde bekliyor; kesme isteği ancak bu kayıttan sonra gönderiliyor. Kayıt satırı atlanıp t.interrupt() doğrudan t.start()’ın hemen ardından çağrılsaydı, isteğin gövde henüz while döngüsüne girmeden mi girdikten sonra mı geldiği belirsiz kalırdı — sonuç değişmezdi (döngü er ya da geç bayrağı görüp çıkardı), ama hangi turda çıktığı ölçülemezdi. Bu derste öyle bir tur sayısı hiç iddia edilmiyor; iddia yalnız çıktı mı çıkmadı mı sorusuyla sınırlı tutuluyor, çünkü sınırın kendisi tam olarak budur: ölçülebilecek olan, kurgunun deterministik kıldığı kadarıdır.

Özet

  • Aynı Runnable gövdesi run() ile çağrılırsa çağıranda, start() ile çağrılırsa oluşturulan bir iş parçacığında koşar; fark çağrı yazımında tek bir yöntem adıdır.
  • isAlive() bir iş parçacığının başlamadan önce, çalışırken ve join() sonrası durumunu belirlenimci biçimde gözler; ikinci start() çağrısı nesnenin durumundan bağımsız olarak her zaman IllegalThreadStateException fırlatır.
  • Kesme bir istektir, bir buyruk değildir: üç kurulumun yalnız biri (bayrağı döngü koşulunda denetleyen) kesme isteği olmadan gövdesini hiç bitiremiyor; kalan ikisi isteği görsün ya da görmesin aynı sonraki adıma ulaşıyor.
  • start()’ın dönmesi, gövdenin ilk satırının koştuğu anlamına gelmez; iki olay arasında sıra kurulmamıştır ve bu, kurgusuz gözlenemez — sıra CountDownLatch ve join ile elle kurulur.

Sonraki Adım

Bu derste “kim koşturuyor” sorusu yanıtlandı ve kesme isteğine verilecek yanıtın gövdenin kendi kodunda yazıldığı görüldü. Ama Kesme derslerinde her kurulum, gövdenin yazdığı bir StringBuilder’ı ve bir sayacı okurken hiçbir eşitleme kullanmadı, çünkü değeri hep aynı iş parçacığı hem yazdı hem okudu — basladi.await() ile t.join() yalnız sırayı kurdu, görünürlüğü sağlayan gerçek mekanizma her ikisinin de kendi içinde zaten taşıdığı bir garantiydi ve bu derste hiç adı konmadı. Sıradaki ders bu rahatlığı kaldırıyor: bir değeri yazan iş parçacığı ile okuyan iş parçacığı farklı olduğunda ne garanti ediliyor, ve bu garantiyi hangi taraf — yalnız yazan mı, yalnız okuyan mı, ikisi de mi — kurmak zorunda?

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat