İçeriğe geç
academia.sh

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 bir Thread‘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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat