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.tanitile kaydolmadıysabilinmeyensayılır. - EZ2 —
Govdeler.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
- EZ3 —
isAlive()iş parçacığının kendisinden hiçbir kimlik verisi okumaz, yalnız “başlatıldı ve henüz bitmedi mi” sorusunatrue/falsedö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ı
Runnablegövdesirun()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 vejoin()sonrası durumunu belirlenimci biçimde gözler; ikincistart()çağrısı nesnenin durumundan bağımsız olarak her zamanIllegalThreadStateExceptionfı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ıraCountDownLatchvejoinile 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.