Ders 10 / 11
Sanal İş Parçacıkları
Aynı görev kümesinin sabit havuzda dörtle sınırlı kalan, sanal iş parçacığı başına bir yürütücüde görev sayısı kadar büyüyen ayrık kimliği; sanal iş parçacığının isVirtual() ile ayırt edilen gerçek bir Thread olup platform iş parçacığı olmadığı; taşıyıcının gözlemlenemediği; ThreadLocal'ın sanal iş parçacığı başına ayrı bir değer tutmasının bir bedel olduğu ölçülür.
İçindekiler
Önceki ders havuzun iş parçacıklarını sınırlı bir kaynak olarak ele aldı: sabit sayıda vardılar, doldular, sırayla yeniden kullanıldılar, doluluk çağıranı işe koştu. Bu sınır, platform iş parçacığının kendisinin pahalı olmasından geliyordu — her biri işletim sisteminin ayırdığı bir yığıt ve bir çizelgeleme kaydı taşıyor. Bu ders aynı soruyu bambaşka bir öncülle soruyor: ya iş parçacığının kendisi ucuzsa? Sanal iş parçacığı, işletim sisteminden değil sanal makineden gelen, çok daha ucuz bir iş parçacığı türü. Aynı görev kümesi iki ayrı yürütücüde koşturuluyor ve aynı soru yeniden soruluyor — bu adımı hangi iş parçacığı koşturuyor — ama bu kez yanıtın kaç ayrı iş parçacığı üretebileceği de ölçülüyor.
Önceki derste yürütücü arayüzü sabit kalmıştı, değişen yalnızca kurucuya verilen sayıydı.
Bu derste arayüz yine aynı — ExecutorService, submit, invokeAll — ama yürütücünün
kendisi bambaşka bir kaynaktan iş parçacığı üretiyor. Aynı arayüzün arkasında iki ayrı
üretim düzeneği durabiliyor olması, bu dersin ölçtüğü ilk şey.
Aynı Görev Kümesi İki Yürütücüde: Ayrık Kimlik Sayısı
Önceki dersin ayrık kimlik sayma tekniği aynen sürüyor: her görev koştuğu iş parçacığını
yalnızca kimliğiyle (== eşdeğeri) bir kümeye ekliyor, ad ya da numara basılmıyor.
EZ22. Ayrık kimlik sayımı sanal iş parçacıkları için de geçerlidir; Thread sınıfının
bir alt tipi olarak sanal iş parçacığı da bir nesne kimliği taşır ve aynı kümeyle sayılabilir.
EZ23. Sanal iş parçacığı başına bir yürütücü (newVirtualThreadPerTaskExecutor), kendisine
verilen her görev için yeni bir sanal iş parçacığı oluşturur; bu, yürütücünün yapısal bir
kuralıdır ve çizelgelemeye bağlı değildir.
// Kimlikler.java — ayrik is parcacigi kimligini == esdegeriyle sayan kume (yardimci kaynak, main yok)
class Kimlikler {
static java.util.Set<Thread> yeniKume() {
return java.util.Collections.synchronizedSet(
java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>()));
}
}
// IkiYurutucu.java — ayni gorev kumesi sabit havuzda ve sanal yurutucude ayri ayrik kimlik sayisi uretiyor
import java.util.*;
import java.util.concurrent.*;
public class IkiYurutucu {
static int ayrikKimlik(ExecutorService yurutucu, int gorevSayisi) throws Exception {
Set<Thread> gorulen = Kimlikler.yeniKume();
List<Callable<Void>> gorevler = new ArrayList<>();
for (int i = 0; i < gorevSayisi; i++) {
gorevler.add(() -> { gorulen.add(Thread.currentThread()); return null; });
}
yurutucu.invokeAll(gorevler);
return gorulen.size();
}
public static void main(String[] args) throws Exception {
try (ExecutorService sabit4 = Executors.newFixedThreadPool(4);
ExecutorService sanal = Executors.newVirtualThreadPerTaskExecutor()) {
System.out.println("4 gorev, sabit havuz(4) -> ayrik kimlik: " + ayrikKimlik(sabit4, 4));
System.out.println("4 gorev, sanal yurutucu -> ayrik kimlik: " + ayrikKimlik(sanal, 4));
}
try (ExecutorService sabit4b = Executors.newFixedThreadPool(4);
ExecutorService sanalb = Executors.newVirtualThreadPerTaskExecutor()) {
System.out.println("20 gorev, sabit havuz(4) -> ayrik kimlik: " + ayrikKimlik(sabit4b, 20));
System.out.println("20 gorev, sanal yurutucu -> ayrik kimlik: " + ayrikKimlik(sanalb, 20));
}
}
}
4 gorev, sabit havuz(4) -> ayrik kimlik: 4 4 gorev, sanal yurutucu -> ayrik kimlik: 4 20 gorev, sabit havuz(4) -> ayrik kimlik: 4 20 gorev, sanal yurutucu -> ayrik kimlik: 20
Dört görevle iki yürütücü de dört ayrı kimlik üretiyor — görev sayısı havuz büyüklüğüne eşit olduğunda fark görünmüyor. Yirmi görevde fark açılıyor: sabit havuz yine dört ayrı kimlikte kalıyor, çünkü önceki dersteki kural burada da geçerli — havuz büyüklüğü kaç görev geldiğinden bağımsız bir üst sınır. Sanal yürütücü ise tam yirmi ayrı kimlik üretiyor; her görev kendi sanal iş parçacığını buluyor, hiçbiri bir öncekini bekleyip onun yerini almıyor. Sayı görev sayısıyla birlikte büyüyor, çünkü sanal iş parçacığı yürütücüsü bir havuz değil — her görev için yeni bir iş parçacığı doğuran bir düzenek (EZ23).
Yirmi sayısı rastgele seçilmedi: dört, havuz büyüklüğüyle örtüştüğü için farkı göstermiyor;
yirmi, havuz büyüklüğünün birkaç katı olduğu için sabit havuzun tavanına çarpıp orada
kaldığını görünür kılıyor. Önceki derste bu tavan bir dolulukla, execute çağrısının
çağırana düşmesiyle ölçülmüştü; burada aynı tavan bir üretim sınırı olarak, ayrık kimlik
sayısı üzerinden ölçülüyor.
Sanal İş Parçacığı da Bir İş Parçacığıdır
Ayrık kimliği büyüten şeyin gerçekten bir iş parçacığı olup olmadığı ayrıca doğrulanabilir.
EZ24. Bir iş parçacığının sanal olup olmadığı yalnızca Thread.isVirtual() ile ayırt
edilir; ad, öncelik ya da başka hiçbir gözlemle değil.
// IsVirtualKontrolu.java — sanal is parcacigi Thread.isVirtual() ile ayirt edilen gercek bir Thread'dir
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
public class IsVirtualKontrolu {
static int sanalSayisi(ExecutorService yurutucu, int gorevSayisi) throws Exception {
AtomicInteger sanal = new AtomicInteger(0);
List<Callable<Void>> gorevler = new ArrayList<>();
for (int i = 0; i < gorevSayisi; i++) {
gorevler.add(() -> {
if (Thread.currentThread().isVirtual()) sanal.incrementAndGet();
return null;
});
}
yurutucu.invokeAll(gorevler);
return sanal.get();
}
public static void main(String[] args) throws Exception {
try (ExecutorService sabit4 = Executors.newFixedThreadPool(4)) {
System.out.println("sabit havuz(4), 4 gorev -> sanal is parcaciginda kosan: "
+ sanalSayisi(sabit4, 4) + "/4");
}
try (ExecutorService sanal = Executors.newVirtualThreadPerTaskExecutor()) {
System.out.println("sanal yurutucu, 4 gorev -> sanal is parcaciginda kosan: "
+ sanalSayisi(sanal, 4) + "/4");
}
Thread anaIsParcacigi = Thread.currentThread();
System.out.println("ana is parcacigi sanal mi : "
+ anaIsParcacigi.isVirtual());
}
}
sabit havuz(4), 4 gorev -> sanal is parcaciginda kosan: 0/4 sanal yurutucu, 4 gorev -> sanal is parcaciginda kosan: 4/4 ana is parcacigi sanal mi : false
Sabit havuzun dört görevinin hiçbiri sanal iş parçacığında koşmuyor, sanal yürütücünün
dördü de koşuyor; kodu koşturan görev aynı lambda, değişen yalnızca yürütücü. main
metodunu koşturan iş parçacık da sanal değil — programın kendisi her zaman bir platform iş
parçacığından başlıyor. Sanal iş parçacığı Thread sınıfının bir alt tipi olarak var
oluyor: aynı start, join, interrupt çağrılarını, aynı yaşam döngüsünü paylaşıyor, ama
platform iş parçacığı değil — yani işletim sisteminin ayırdığı bir iş parçacığına
birebir karşılık gelmiyor. Bu ayrımın önceki bölümdeki sayıyla ilişkisi doğrudan: sanal
yürütücü yirmi ayrı kimlik üretebiliyordu çünkü ürettiği şey işletim sisteminin ayırdığı bir
kaynak değil, sanal makinenin kendi yönettiği bir nesne. isVirtual() bu iki türü
ayırmanın tek yolu; ikisi de aynı üst sınıftan geldiği ve aynı yöntemleri barındırdığı için
başka hiçbir çağrı bu ayrımı vermiyor.
Engellenen Sanal İş Parçacığı Taşıyıcısından İner
Bir platform iş parçacığı yoksa sanal iş parçacığının kodu nasıl koşuyor? Sanal iş
parçacığı, koşarken bir taşıyıcı iş parçacığına biniyor — kod aslında bir platform iş
parçacığı üzerinde çalışıyor, sanal iş parçacığı o platform iş parçacığını ödünç alıyor.
Sanal iş parçacığı CountDownLatch.await() gibi bir çağrıda engellenirse, taşıyıcısından
iniyor: altındaki platform iş parçacığı serbest kalıyor ve başka bir sanal iş
parçacığının kodunu koşturabiliyor. Engelleme bitince sanal iş parçacığı yeniden bir
taşıyıcıya biniyor — zorunlu olarak aynısına değil.
Binme ve inme, CountDownLatch, kilitler ve engelleyen girdi-çıktı gibi standart
kütüphane çağrılarında düzenek tarafından yönetiliyor; çağıran bu düzeneği kurmuyor, yalnız
her zamanki gibi await() ya da get() çağırıyor. Bir sanal iş parçacığının kaç kere binip
indiği de gözlemlenemeyen bir şey — bu ders o sayıyı da ölçmüyor, yalnızca kaç ayrı sanal iş
parçacığı kimliğinin aynı anda var olabildiğini ölçüyor.
Taşıyıcı iş parçacığı gözlemlenemez — bu ders onu ne yazdırıyor ne de sayıyor; ölçülen
şey yalnızca ayrık sanal iş parçacığı kimliği sayısı. EZ25. Elli görev, hepsi bir
CountDownLatch üzerinde engellenip aynı anda serbest bırakılacak biçimde kuruluyor;
serbest bırakma çağrısı görevlerin tamamı gönderildikten hemen sonra yapılıyor, bu yüzden
gecikme bir yarış değil — CountDownLatch sıfıra indiğinde bekleyen her görev, ne zaman
kontrol ederse etsin aynı yanıtı görüyor.
// EngellenenGorevler.java — elli engelleyen gorev sabit havuzda dort, sanal yurutucude elli ayrik kimlik uretiyor
import java.util.*;
import java.util.concurrent.*;
public class EngellenenGorevler {
static int engellenerekAyrikKimlik(ExecutorService yurutucu, int gorevSayisi) throws Exception {
Set<Thread> gorulen = Kimlikler.yeniKume();
CountDownLatch birak = new CountDownLatch(1);
List<Future<?>> gelecekler = new ArrayList<>();
for (int i = 0; i < gorevSayisi; i++) {
gelecekler.add(yurutucu.submit(() -> {
try {
birak.await();
} catch (InterruptedException e) { }
gorulen.add(Thread.currentThread());
}));
}
birak.countDown();
for (Future<?> g : gelecekler) g.get();
return gorulen.size();
}
public static void main(String[] args) throws Exception {
int gorevSayisi = 50;
try (ExecutorService sabit4 = Executors.newFixedThreadPool(4)) {
System.out.println(gorevSayisi + " engelleyen gorev, sabit havuz(4) -> ayrik kimlik: "
+ engellenerekAyrikKimlik(sabit4, gorevSayisi));
}
try (ExecutorService sanal = Executors.newVirtualThreadPerTaskExecutor()) {
System.out.println(gorevSayisi + " engelleyen gorev, sanal yurutucu -> ayrik kimlik: "
+ engellenerekAyrikKimlik(sanal, gorevSayisi));
}
}
}
50 engelleyen gorev, sabit havuz(4) -> ayrik kimlik: 4 50 engelleyen gorev, sanal yurutucu -> ayrik kimlik: 50
Elli görev de bir kilit noktasında engelleniyor, sonra hep birlikte serbest bırakılıyor. Sabit havuzda dört platform iş parçacığı bu elli görevi sırayla, dördü bir kerede işliyor — ayrık kimlik dörtte kalıyor. Sanal yürütücüde ellisi de kendi sanal iş parçacığını buluyor ve hepsi tamamlanıyor; hiçbiri “iş parçacığı kalmadı” diye kuyrukta beklemiyor. Bu, dört platform iş parçacığıyla kurulu bir taşıyıcı havuzunun elli sanal iş parçacığını karşılayabildiğini gösteriyor — engellenen bir sanal iş parçacığı taşıyıcısını serbest bıraktığı için. Sınırlayıcı ölçüm: ayrık kimlik sayısının büyümesi bir hız iddiası değil, bir olgudur. Bu derste hiçbir süre ölçülmüyor; ölçülen tek şey kaç ayrı sanal iş parçacığı kimliğinin var olabildiği. Bunun bir kazanca dönüşüp dönüşmediği ancak engelleyen kodun kaç iş parçacığını meşgul ettiği sorusuyla anlamlıdır, ve o soru burada modelleniyor, koşturulmuyor: elli görevin hepsi platform iş parçacığı sayısını aşan bir “aynı anda engellenmiş görev” sayısına ulaşabildi, oysa sabit havuzda böyle bir sayı hiçbir zaman dörtten büyük olamazdı.
Bu modelleme kasıtlı bir sınır çiziyor: elli sayısını yüz ya da bin yapmak sonucu değiştirmez, yalnızca aynı olguyu daha büyük bir sayıyla tekrarlar. Ders burada durup büyütmüyor, çünkü iddia zaten sayının kendisinde değil, sabit havuzun asla aşamadığı bir tavanın sanal yürütücüde hiç var olmamasında. Bir platform iş parçacığı engellendiğinde kendi yığıtını ve çizelgeleme kaydını tutmaya devam eder; sanal iş parçacığı engellendiğinde altındaki platform iş parçacığını bırakır ve geriye yalnızca sanal makinenin tuttuğu küçük bir durum kalır — taşıyıcının kaç kez el değiştirdiği bu derste sayılmayan, ama yukarıdaki “elli tamamlandı” sonucunun arkasında duran şey.
ThreadLocal Görev Başına Ayrı Değer Tutar: Bir Bedel
Sanal iş parçacığının bu bolluğunun bir bedeli var. ThreadLocal, her iş parçacığına kendi
değerini veren bir yapı; değer ilk get() çağrısında initialValue ile üretiliyor ve o iş
parçacığına bağlı kalıyor.
// OzelDegerBedeli.java — ThreadLocal her sanal is parcacigina ayri bir deger uretiyor, sabit havuzda paylasiliyor
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
public class OzelDegerBedeli {
static int uretilenDegerSayisi(ExecutorService yurutucu, int gorevSayisi) throws Exception {
AtomicInteger sayac = new AtomicInteger(0);
ThreadLocal<Integer> ozel = ThreadLocal.withInitial(sayac::incrementAndGet);
List<Callable<Void>> gorevler = new ArrayList<>();
for (int i = 0; i < gorevSayisi; i++) {
gorevler.add(() -> { ozel.get(); return null; });
}
yurutucu.invokeAll(gorevler);
return sayac.get();
}
public static void main(String[] args) throws Exception {
int gorevSayisi = 10;
try (ExecutorService sabit4 = Executors.newFixedThreadPool(4)) {
System.out.println(gorevSayisi + " gorev, sabit havuz(4) -> uretilen ozel deger: "
+ uretilenDegerSayisi(sabit4, gorevSayisi));
}
try (ExecutorService sanal = Executors.newVirtualThreadPerTaskExecutor()) {
System.out.println(gorevSayisi + " gorev, sanal yurutucu -> uretilen ozel deger: "
+ uretilenDegerSayisi(sanal, gorevSayisi));
}
}
}
10 gorev, sabit havuz(4) -> uretilen ozel deger: 4 10 gorev, sanal yurutucu -> uretilen ozel deger: 10
ozel değişkeni tek bir ThreadLocal nesnesi, ikisinde de aynı kod satırından okunuyor.
Sabit havuzda on görev dört platform iş parçacığını paylaşıyor; her platform iş parçacığı
kendi değerini bir kez üretip sonraki görevlerde onu yeniden kullanıyor — toplam dört
değer üretiliyor. Sanal yürütücüde on görevin her biri kendi sanal iş parçacığında koştuğu
için initialValue on kez çağrılıyor, hiçbir değer paylaşılmıyor. Bu, önceki bölümün
ayrık kimlik sayısıyla aynı yönde bir sonuç ama tersine okunması gereken bir sonuç: sanal iş
parçacığının bol olması, ThreadLocal kullanan koda o bolluk kadar üretim yaptırıyor.
Görev başına bir sanal iş parçacığı çalıştırmak, görev başına bir ThreadLocal değeri
kurmak demek olabiliyor — bu bir bedel, ve büyük görev sayılarında gözden kaçırılabilir.
Dört ölçüm birlikte okununca kurs şu iddiaya varıyor: sanal iş parçacığı, iş parçacığının
kendisini ucuzlatıyor ama iş parçacığıyla birlikte gelen her şeyi ucuzlatmıyor.
ThreadLocal bunun ölçülen tek örneği; iş parçacığı sayısına bağlı büyüyen başka her yapı
aynı bedeli taşıyabilir. Ayrık kimlik sayısı büyüdükçe bu bedel de büyür — kazancın kendisi
gibi, bedel de bir olgudur ve ayrı ölçülmesi gerekir.
Özet
- Aynı görev kümesi sabit havuzda havuz büyüklüğüyle sınırlı kalan, sanal iş parçacığı başına bir yürütücüde görev sayısı kadar büyüyen bir ayrık kimlik sayısı üretir.
- Sanal iş parçacığı
Thread.isVirtual()ile ayırt edilen gerçek birThread‘dir; aynı yaşam döngüsü API’sini paylaşır ama bir platform iş parçacığına birebir karşılık gelmez. - Engellenen bir sanal iş parçacığı taşıyıcısından iner ve platform iş parçacığını serbest bırakır; taşıyıcı gözlemlenemez, ölçülen şey yalnızca ayrık sanal kimlik sayısıdır.
- Ayrık kimlik sayısının büyümesi bir hız iddiası değildir, bir olgudur; kazanç ancak engelleyen kodun kaç iş parçacığını meşgul ettiği sorusuyla anlamlıdır ve bu soru modellenir, koşturulmaz.
ThreadLocal, sabit havuzda değeri iş parçacıkları arasında paylaştırırken sanal yürütücüde görev başına ayrı bir değer üretir; bu, sanal iş parçacığının bolluğunun taşıdığı bir bedeldir.
Sonraki Adım
Bu ders iş parçacığının kendisini ucuzlattı ve görev başına bir tane doğurmanın mümkün olduğunu gösterdi. Ama şimdiye kadarki bütün görevler bağımsızdı — biri bitmeden öteki başlamıyordu. Sıradaki ders bağımsız görevleri bir zincire diziyor: bir adımın sonucu bir sonrakinin girdisi oluyor. Zincire dizilen bu aşamaları kim koşturuyor sorusu, artık tek bir görev değil ardışık bir dizi için soruluyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.