İçeriğe geç
academia.sh

Ders 11 / 11

Eşzamansız Bileşim

Gelecek tabanlı bir zincirde her aşamayı hangi tarafın koşturduğu — tamamlanmış gelecekte çağıran, bekleyen gelecekte tamamlayan taraf, sonekli çağrıda hiç oluşturmadığımız ya da verilen bir havuz — ve aynı zincirin dört yazımda dört ayrı tarafta koşup sonucun dördünde de aynı kaldığı ölçülür. Bir aşama düşerse sonraki aşamaların atlandığı ve hatanın join'de çağırana sarmalanmış geldiği gösterilir.

İçindekiler

Önceki ders bağımsız görevleri ucuzlattı; bu ders onları bir zincire diziyor. Bir aşamanın sonucu bir sonrakinin girdisi oluyor ve her halka kendi başına doğru çalışsa da zincire dizildiğinde yeni bir soru doğuyor: bu aşamayı hangi iş parçacığı koşturuyor? Gelecek (future) tabanlı zincirleme bu soruyu her bağlama noktasında yeniden soruyor, ve kursun ölçtüğü dört yanıt burada aynı zincir üzerinde bir arada görünüyor.

Aynı Zincir Dört Yazımda Dört Ayrı Tarafta Koşuyor

Bir aşama, bağlandığı CompletableFuture zaten tamamlanmışsa bağlama çağrısını yapan iş parçacığında senkron koşuyor; gelecek henüz tamamlanmamışsa aşama, geleceği tamamlayan tarafın iş parçacığında koşuyor. Async sonekli bir bağlama ise üçüncü bir yolu açıyor: yürütücü verilmezse ortak bir havuzda, verilirse verilen yürütücüde.

EZ26. Bir aşama, bağlandığı gelecek zaten tamamlanmışsa bağlama çağrısını yapan iş parçacığında senkron koşar; gelecek henüz tamamlanmamışsa aşama, geleceği tamamlayan iş parçacığında koşar. EZ27. Async sonekli bir aşama, yürütücü verilmezse programın hiç oluşturmadığı ortak bir havuzda (bilinmeyen), yürütücü verilirse verilen yürütücünün iş parçacığında (oluşturulan) koşar.

// Kosum.java — bir adimi hangi is parcacigi kosturuyor: cagiran mi, olusturulan mi, hic olusturmadigimiz bir taraf mi (yardimci kaynak, main yok)
import java.util.*;

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());
    }
}
// DortYazim.java — ayni zincir dort ayri yazimda dort ayri tarafta kosuyor, sonuc dordunde de ayni
import java.util.*;
import java.util.concurrent.*;

public class DortYazim {
    static int asamaFonksiyonu(int v) { return v * 2 + 1; }

    public static void main(String[] args) throws Exception {
        Kosum.tanit("cagiran");
        List<Integer> sonuclar = new ArrayList<>();

        // A. tamamlanmis gelecek: adim cagiranda kosuyor
        int a = CompletableFuture.completedFuture(10)
                .thenApply(v -> { Kosum.gozle("tamamlanmis gelecek adimi"); return asamaFonksiyonu(v); })
                .join();
        sonuclar.add(a);

        // B. bekleyen gelecek: adimi tamamlayan is parcacigi kosturuyor
        CompletableFuture<Integer> bekleyen = new CompletableFuture<>();
        CompletableFuture<Integer> zincir = bekleyen.thenApply(v -> {
            Kosum.gozle("bekleyen gelecek adimi"); return asamaFonksiyonu(v);
        });
        Thread tamamlayan = new Thread(() -> { Kosum.tanit("olusturulan"); bekleyen.complete(10); });
        tamamlayan.start();
        tamamlayan.join();
        sonuclar.add(zincir.join());

        // C. varsayilan havuzda async: hic olusturmadigimiz bir is parcaciginda
        int c = CompletableFuture.completedFuture(10)
                .thenApplyAsync(v -> { Kosum.gozle("varsayilan havuzda async"); return asamaFonksiyonu(v); })
                .join();
        sonuclar.add(c);

        // D. verilen havuzda async: kurdugumuz yuruticunun is parcaciginda
        ExecutorService verilen = Executors.newSingleThreadExecutor(
                gorev -> new Thread(() -> { Kosum.tanit("olusturulan"); gorev.run(); }));
        int d = CompletableFuture.completedFuture(10)
                .thenApplyAsync(v -> { Kosum.gozle("verilen havuzda async"); return asamaFonksiyonu(v); }, verilen)
                .join();
        sonuclar.add(d);
        verilen.shutdown();

        boolean hepsiAyni = sonuclar.stream().distinct().count() == 1;
        System.out.println("dort yazimin sonucu (10*2+1=21)  : " + sonuclar);
        System.out.println("hepsi ayni mi                    : " + hepsiAyni);
    }
}
tamamlanmis gelecek adimi        -> cagiran
bekleyen gelecek adimi           -> olusturulan
varsayilan havuzda async         -> bilinmeyen
verilen havuzda async            -> olusturulan
dort yazimin sonucu (10*2+1=21)  : [21, 21, 21, 21]
hepsi ayni mi                    : true

Dört satır kursun dört ayrı yanıtını tek zincirde topluyor. Tamamlanmış bir gelecekte thenApply bağlanır bağlanmaz koşuyor, çünkü koşacak bir şey zaten hazır — bağlama çağrısını yapan iş parçacığı hangisiyse aşama onda koşuyor: çağıran (EZ26). Bekleyen bir gelecekte aşama beklemede kalıyor; bekleyen.complete(10) çağrısı gelene kadar hiçbir iş parçacığında koşmuyor, çağrı geldiğinde de onu çağıran iş parçacığında koşuyor — burada o taraf tamamlayan adıyla kurduğumuz iş parçacığı, yani oluşturulan (EZ26). Sonek eklenen üçüncü satırda yürütücü verilmediği için aşama ortak bir havuzda koşuyor; bu havuzu biz ne kurduk ne adlandırdık, bu yüzden bilinmeyen (EZ27). Dördüncü satırda aynı sonek bu kez bir yürütücüyle veriliyor ve aşama o yürütücünün — bizim kurduğumuz — iş parçacığında koşuyor: oluşturulan (EZ27).

Sonek Eklemek Koşturan Tarafı Değiştirir, Hızlandırmaz

Dört yazımın da uyguladığı işlem birebir aynı: v -> v * 2 + 1, aynı başlangıç değeri üzerinde. Sonuç listesi [21, 21, 21, 21] — dördü de aynı sayıya varıyor. Bu, dersin sınırlayıcı ölçümü: thenApply ile thenApplyAsync arasındaki fark, hesaplanan değerde değil, o değeri kimin hesapladığında. Sonek eklemek işi hızlandırmadı; bu derste hiçbir süre ölçülmedi ve ölçülemez de — değişen tek şey çağrı yazımının hangi tarafı seçtiği. Bir aşamayı Async sonekiyle yazmanın gerekçesi hız değil, o aşamayı çağıranın iş parçacığından ayırmak: örneğin çağıran bir arayüz iş parçacığıysa ya da uzun sürebilecek bir işi kendi üzerinde tutmak istemiyorsa. Kararın kendisi bu dersin konusu değil; ölçülen yalnızca kararın sonucu, yani hangi tarafın koştuğu.

Bir Aşama Düşünce Sonrakiler Atlanıyor

Zincirdeki bir aşama istisna fırlatırsa ne olur? Sıradaki aşamalar hiç çağrılmıyor ve hata, zincirin ucundaki join() çağrısına kadar taşınıyor.

EZ28. thenApply zincirinde her aşama bir öncekinin tamamlandığı iş parçacığında senkron koşar; bu zincirde hiçbir Async soneki yok, dolayısıyla hata yolunun hangi iş parçacığında koştuğu ayrıca bir kurguya ihtiyaç duymaz — zaten tamamlanmış bir gelecekte bu iş parçacığı çağırandır.

// HataYolu.java — bir asama duserse sonraki asamalar atlaniyor, hata join'de cagirana sarmalanmis geliyor
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

public class HataYolu {
    public static void main(String[] args) throws Exception {
        AtomicInteger calisanAsama = new AtomicInteger(0);

        CompletableFuture<Integer> zincir = CompletableFuture.completedFuture(10)
                .thenApply(v -> { calisanAsama.incrementAndGet(); return v + 1; })
                .thenApply(v -> { if (v > 0) throw new RuntimeException("asama hatasi"); return v; })
                .thenApply(v -> { calisanAsama.incrementAndGet(); return v + 1; })
                .thenApply(v -> { calisanAsama.incrementAndGet(); return v + 1; });

        try {
            zincir.join();
            System.out.println("zincir dusmedi");
        } catch (CompletionException e) {
            System.out.printf("%-32s -> %s sarmaliyor: %s%n", "hata join'de yakalandi",
                    e.getClass().getSimpleName(), e.getCause().getClass().getSimpleName());
        }

        int hatadanSonrakiTanimliAsama = 2;
        int hatadanSonrakiCalisanAsama = calisanAsama.get() - 1;
        System.out.printf("%-32s -> %d%n", "hatadan once calisan asama", 1);
        System.out.printf("%-32s -> %d%n", "hatadan sonra tanimli asama", hatadanSonrakiTanimliAsama);
        System.out.printf("%-32s -> %d%n", "hatadan sonra atlanan asama",
                hatadanSonrakiTanimliAsama - hatadanSonrakiCalisanAsama);
    }
}
hata join'de yakalandi           -> CompletionException sarmaliyor: RuntimeException
hatadan once calisan asama       -> 1
hatadan sonra tanimli asama      -> 2
hatadan sonra atlanan asama      -> 2

Zincir dört aşamadan kuruluydu: birincisi normal koştu, ikincisi bir istisna fırlattı, üçüncü ve dördüncüsü hiç koşmadı. Sayaç bunu doğruluyor — hatadan önce bir aşama çalıştı, hatadan sonra tanımlı iki aşamanın ikisi de atlandı. thenApply zinciri bir aşama düştüğünde geri kalanı otomatik olarak düşmüş sayıyor; her aşama kendinden öncekinin başarıyla tamamlandığını varsayıyor, bu varsayım kırılınca zincirin geri kalanı hiç değerlendirilmiyor. Hata join() çağrısına kadar sessizce taşınıyor ve orada CompletionException içine sarmalanmış olarak çağırana ulaşıyor; asıl istisna getCause() ile geri alınabiliyor. Bu davranış önceki dersin submit/get çiftiyle aynı yönde: bir istisna, verildiği biçimin izin verdiği ilk noktaya kadar sessiz kalabiliyor.

İki dersin istisna taşıma biçimi de birbirine yakın ama özdeş değil. Önceki ders Future.get() çağrıldığında istisnayı denetimli ExecutionException içine sarıyordu; bu ders join() çağrıldığında aynı işi denetimsiz CompletionException ile yapıyor. İkisi de gerçek istisnayı getCause() ile geri veriyor, ikisi de görevin/aşamanın çalıştığı taraftan çağıranın tarafına bir sarmalamayla geçiyor — fark yalnız hangi çağrının (get mi join mi) hangi sarmalamayı (denetimli mi denetimsiz mi) seçtiğinde.

Özet

  • Bir aşama, bağlandığı gelecek zaten tamamlanmışsa bağlama çağrısını yapan iş parçacığında senkron koşar; gelecek henüz tamamlanmamışsa aşama, geleceği tamamlayan iş parçacığında koşar.
  • Async sonekli bir aşama yürütücü verilmezse programın hiç oluşturmadığı ortak bir havuzda, verilirse verilen yürütücünün iş parçacığında koşar.
  • Aynı zincir dört ayrı yazımla kurulduğunda dört ayrı taraf koşturur, ama uygulanan işlem aynı olduğu için sonuç dördünde de birebir aynı kalır; sonek eklemek koşturan tarafı değiştirir, hızlandırmaz.
  • Bir aşama istisna fırlattığında zincirdeki sonraki bütün aşamalar atlanır; hata join() çağrısına kadar sessizce taşınır ve orada CompletionException içine sarmalanmış olarak çağırana ulaşır.
  • Kesilen bir zincirde hatadan önceki aşamalar normal biçimde çalışmış olur; atlanan aşama sayısı zincirin geri kalanının uzunluğuna eşittir.

Kurs Kapanışı

EZ29. Kapanış tablosundaki koşturan taraf sütunu yalnızca üç değeri alır: çağıran, oluşturulan, bilinmeyen — bunlar ortak tanımın kurduğu üç yanıttır ve kurs boyunca sabit kalır.

Kurs tek bir soruyla açıldı: bu adımı hangi iş parçacığı koşturuyor, ve o iş parçacığını kim oluşturdu? On bir ders bu soruyu on bir ayrı düzenekte sordu ve yanıtı her seferinde en az iki koşum yolunu yan yana koşturarak aldı. Birinci iddia doğrulandı ve bu derste bir kez daha görüldü: koşturan iş parçacığı çağrı yazımında görünmüyor — thenApply ile thenApplyAsync arasındaki fark bir sonek, ama koşturan taraf üçe ayrılıyor. İkinci iddia — programın hiç oluşturmadığı bir iş parçacığının kendi kodunu koşturabilmesi — bu derste ortak havuzla bir kez daha örneklendi. Üçüncü iddia, K04’e özgü ayrım olan “kim koşturuyor” ile “kim görüyor” sorusunun ayrılığı, önceki derslerde bellek modeli ve ThreadLocal üzerinden ölçülmüştü; bu ders o ayrımı sınamadı, yalnız koşturan tarafı sınadı. Kursun sınırı da baştan beri aynıydı ve burada bir daha doğrulandı: sonek eklemek, havuz büyütmek, sanal iş parçacığı kullanmak — hiçbiri tek başına bir hız iddiası değil; ölçülen her şey bir sayıydı, bir süre değil.

Ders Ölçülen davranış Koşturan taraf (çağıran · oluşturulan · bilinmeyen) Sınırlayıcı ölçüm
Sınıf Yükleme Dört tetikleyicinin üçü (yeni örnek, statik yöntem, statik alan okuma) sınıf başlatmasını koşturur; aynı sınıfa aynı anda dokunan iki iş parçacığından yalnız biri başlatma bloğunu çalıştırır çağıran (dört tetikleyici) · oluşturulan (eş zamanlı dokunmada kazanan) Derleme zamanı sabiti okumak sınıfı hiç başlatmaz; boş satır “tetiklenmedi” demektir, “olmadı” değil
Bellek Alanları Altı değerden üçü (yerel değişken, parametre, iş parçacığına özel değer) yazan iş parçacığının dışına hiç çıkmaz, üçü (nesne alanı, dizi ögesi, statik alan) çıkar oluşturulan (yazan) · çağıran (okuyan, join sonrası) Paylaşılan olmak görülecek olmak değildir; join gibi bir sıralama aracı olmadan aynı okuma tanımsızdır
Çöp Toplayıcılar Güçlü referans dururken üç toplama isteğinin üçünde de temizleme koşmaz; referans kaldırılınca koşar. Zayıf ve hayalet referans kuyruğa düşer, yumuşak direnir bilinmeyen (temizleme eylemi) System.gc() bir istektir; ölçüm temizlemenin ne zaman koşacağını değil, yalnız kimin koşturduğunu saptar
Anında Derleme Aynı yöntem üç yüz bin kez çağrıldıktan sonra da sınıf dosyasındaki komut listesi birebir aynı kalır; çağrı yerinden geçen alıcı tip sayısı birden ikiye çıkınca tek alıcılı varsayım geçersizleşir çağıran (profil verisini üreten) · bilinmeyen (kararı veren) Hız ölçülmez ve ölçülemez; sayılan tek şey komut sayısı ile alıcı tip sayısıdır, optimizasyondan dönüş modellenir
İş Parçacıkları ve Yaşam Döngüsü Aynı Runnable gövdesi run() ile çağıranda, start() ile oluşturulan iş parçacığında koşar; üç kesme kurulumundan yalnız biri kesme isteği olmadan gövdesini bitiremez çağıran (run) · oluşturulan (start) start() çağrısının dönmesi gövdenin koştuğunu göstermez; sıra CountDownLatch ve join ile elle kurulur
Java Bellek Modeli Sekiz yayımlama düzeneğinden dördü hem yazanın hem okuyanın harekete geçmesini istiyor, final alan yükü yalnız yazana, join yükü yalnız okuyana yüklüyor, ikisi (senkronizasyonsuz alan, ThreadLocal) görünürlük sorusunun dışında kalıyor oluşturulan (yazan) · çağıran (okuyan) Senkronizasyonsuz bir alanın görünürlük kusuru koşturularak gösterilmez, kural olarak modellenir; volatile atomiklik vermez
Senkronizasyon Yeniden girme derinliği 0’dan 2’ye çıkıp 0’a iner; üç kilit kurulumunun yalnız birinde tek kilit paylaşılır, kalan ikisinde ayrı kilit sayısı ikiye çıkar çağıran (kilidi tutan) · oluşturulan (bekleyen) Kilit karşılıklı dışlama ve görünürlük verir, bekleyenler arasında sıra vermez; adil kilit seçeneği modellenir
Eşzamanlılık Yardımcıları Karşılaştır-ve-ata kurgulanmış çakışmada üç denemede sonuca ulaşır ve iş çağırandan hiç ayrılmaz; dört anlık görüntü sırasıyla 0, 1, 2 ve 3 öge görür, yazan taraf her değişiklikte yeni bir dizi üretir çağıran (karşılaştır-ve-ata, gezinme) · oluşturulan (sayacı düşüren, izni bırakan) Atomik tip yalnız tek değişkene atomiklik verir; iki değişkenin birlikte tutarlılığı koşturularak değil, kural olarak anlatılır
Yürütücü Servisleri Havuz boşken kendi iş parçacığı, doluyken görevi veren taraf koşturur; reddetme yordamı çağıranın seçtiği bir parametredir çağıran (dolu havuzda reddedilen) · oluşturulan (havuza verilen görev) İki verme biçiminden yalnızca biri (execute) istisnayı kendiliğinden gösterir; submit yalnızca get çağrılırsa gösterir
Sanal İş Parçacıkları Aynı görev kümesinin sabit havuzda dörtle sınırlı kalan, sanal yürütücüde görev sayısı kadar büyüyen ayrık kimliği; ThreadLocal’ın görev başına ayrı değer üretmesi oluşturulan (her ikisinde de) Ayrık kimlik sayısının büyümesi bir hız iddiası değil bir olgudur; taşıyıcı gözlemlenemez
Eşzamansız Bileşim Tamamlanmış gelecekte çağıran, bekleyen gelecekte tamamlayan taraf, sonekli çağrıda bilinmeyen ya da verilen havuz koşar; hatadan sonraki aşamalar atlanır çağıran (tamamlanmış) · oluşturulan (bekleyen, verilen havuz) · bilinmeyen (varsayılan havuz) Sonek eklemek hızlandırmaz, yalnız koşturan tarafı değiştirir; dört yazımın sonucu birebir aynı kalır

JVM, Eşzamanlılık ve Başarım burada kapanıyor. Kursun sorduğu soru — bu adımı hangi iş parçacığı koşturuyor — bir Java programının her satırının altında hep durur; çağrı yazımı bu soruyu hiçbir zaman kendiliğinden yanıtlamaz.

İ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