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.
Asyncsonekli 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 oradaCompletionExceptioniç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.