İçeriğe geç
academia.sh

Ders 21 / 23

Eş Yordamlar

Eş yordamın askıya alındığı noktada sıra verdiğinin, sonucun await ile alındığının ve iki ayrı başlatılmış işin eşit sayılmadığının ölçümü.

Yayın Güncelleme

Bu derste

  • launch, join, async ve await ile başlatılan işlerin sırasını çıktıdan okur
  • suspend ile işaretlenmiş fonksiyonun yalnız eş yordam içinden çağrılabildiğini açıklar
  • İki ayrı async çağrısının Deferred nesnelerinin eşit sayılmadığını ölçer
  • Deferred yerine await sonucunun ve bağlam öğesinin içerikle karşılaştırıldığını gösterir
İçindekiler

Önceki derste tembel dizi, bir hesabın tarifini taşıyordu ve hesap uç işlem gelince çalışıyordu. Bazı hesaplar ise başka bir işin bitmesini bekler: bir dosyanın okunması, bir ağ yanıtı, bir zamanlayıcı. Bu bekleme sırasında çağıranı tutmak istenmez. Kotlin bu durum için eş yordam (coroutine) sunar: bir iş yarıda durabilir, kontrolü bırakabilir ve sonra kaldığı yerden devam edebilir.

Eş yordam kullanan kod kotlinx.coroutines eş yordam kitaplığını içe alır. Bu kitaplık dilin kendisinde değil, ayrı bir pakettedir; bu derste ve bir sonrakinde import kotlinx.coroutines.* satırı gerekir, önceki derslerde gerekmiyordu. Bu derste üç yapı yeter. suspend ile işaretlenmiş askıya alınan fonksiyon, gövdesinde yarıda durabilen fonksiyondur ve yalnız bir eş yordamdan ya da başka bir askıya alınan fonksiyondan çağrılır. launch yeni bir eş yordam başlatır ve bir Job döndürür; sonuç beklenmez, yalnız bitişi join() ile beklenir. async yeni bir eş yordam başlatır ve sonucu taşıyan bir Deferred döndürür; sonuç await() ile alınır. Üçünü de bir eş yordam kapsamı içinde çalıştırmak gerekir, bu derste kapsamı runBlocking kurar: main fonksiyonunu çağıran iş parçacığını, içindeki bütün eş yordamlar bitene kadar bekletir.

Eş yordam bir iş parçacığı değildir. Bir iş parçacığı işletim sisteminin sıraladığı bir yürütme birimidir; eş yordam ise kitaplığın bir dağıtıcı (Dispatchers) üzerinde yürüttüğü iştir ve aynı iş parçacığında sırayla yürüyebilir. Bu derste hiçbir ölçüm süreye ya da iş parçacığı adına dayanmaz: zamanlama çıktıdan okunmaz, yalnız işlerin sırası ve sonuçları okunur. Ölçü sorusu önceki derslerdeki gibi kalır: bu iki değeri eşit yapan ne?

// Criterion.kt — iki degeri esit yapan olcutu siniflandirir
class Criterion(val label: String, first: Any?, twin: Any?, other: Any?) {
    val sameObject = first === twin
    val equalTwin = first == twin
    val equalOther = first == other

    val name: String = when {
        sameObject -> "tekil"
        !equalTwin -> "kimlik"
        !equalOther -> "icerik"
        else -> "kismi-icerik"
    }

    fun report() {
        println("$label: ayni-nesne=$sameObject, ikiz=$equalTwin, farkli=$equalOther -> $name")
    }
}

fun summarize(checks: List<Criterion>) {
    for (name in listOf("tekil", "kimlik", "icerik", "kismi-icerik")) {
        println("$name: ${checks.count { it.name == name }}")
    }
}

Sıra: launch, join, async, await

Aşağıdaki blok doubled adında bir askıya alınan fonksiyon tanımlar; içinde yield() çağrısı eş yordamın sırayı başkasına verebileceği bir nokta kurar. main üç şeyi ölçer. İlk olarak launch bloğunun, launch çağrısını izleyen satırdan önce mi sonra mı çalıştığı: ikisi de aynı listeye kayıt ekler. İkinci olarak iki async işinin sonuçlarının await() ile toplanması. Üçüncü olarak coroutineScope içinde başlatılan üç işin sonuçlarının sırayla alınması.

// Main.kt — launch, async ve await
import kotlinx.coroutines.*

suspend fun doubled(n: Int): Int {
    yield()
    return n * 2
}

fun main() = runBlocking {
    val log = mutableListOf<String>()
    val job = launch { log.add("launch") }
    log.add("main")
    job.join()
    println(log)
    val first = async { doubled(21) }
    val second = async { doubled(4) }
    println(first.await() + second.await())
    val total = coroutineScope {
        val parts = listOf(1, 2, 3).map { async { doubled(it) } }
        parts.map { it.await() }
    }
    println(total)
}
[main, launch]
50
[2, 4, 6]

İlk satır önemli bir sonucu gösterir: launch bloğu, launch çağrısı döner dönmez çalışmadı. Önce main kaydı eklendi, launch kaydı ondan sonra geldi. runBlocking tek bir iş parçacığında çalıştığı için yeni eş yordam, başlatan eş yordam bir askıya alma noktasına gelene kadar sırada bekler; burada bu nokta job.join() çağrısıdır. Sıranın böyle çıkması dağıtıcıya bağlıdır; bu yüzden ders bu sırayı değil, join() çağrısından sonra her iki kaydın da listede olduğunu güvence sayar.

İkinci satırda iki async işi 21 * 2 ve 4 * 2 sonuçlarını üretir; await() bunları alır ve toplam 50 çıkar. Üçüncü satırda coroutineScope, içinde başlatılan bütün işler bitmeden dönmez; üç async’in sonuçları await() ile listede başlatılış sırasında alınır: [2, 4, 6]. Sonuçların sırası başlatma sırasını izler, çünkü parts listesi await() çağrılarını o sırayla yapar. İşlerin hangisinin önce bittiği ise bu çıktıdan okunamaz ve okunmaya çalışılmaz.

KE20. Bu ölçümler runBlocking kapsamı ve tek bir Int sonuç tipi için. Başka bir dağıtıcıda launch ile main kaydının sırası, askıya alma noktası olmayan bir fonksiyonda sıra ve işler arasındaki gerçek bitiş sırası ölçülmedi.

Ölçüt: Kimlik

async bir sonuç değil, sonucun sözünü döndürür. Bu söz bir Deferred nesnesidir ve her async çağrısı yeni bir tane kurar. Aşağıdaki blok iki ayrı async { 1000 } çağrısını ve değeri farklı üçüncüsünü karşılaştırır. İkinci ölçüm sabit küçük bir değerle aynı soruyu sorar. İkisinde de bloklar, sorunun bir sonuç değil bir Deferred olduğunu görmek için await() çağırmaz.

// Main.kt — iki Deferred
import kotlinx.coroutines.*

fun main() = runBlocking {
    val checks = listOf(
        Criterion("async-1000", async { 1000 }, async { 1000 }, async { 2000 }),
        Criterion("async-1", async { 1 }, async { 1 }, async { 2 }),
    )
    for (check in checks) check.report()
    summarize(checks)
}
async-1000: ayni-nesne=false, ikiz=false, farkli=false -> kimlik
async-1: ayni-nesne=false, ikiz=false, farkli=false -> kimlik
tekil: 0
kimlik: 2
icerik: 0
kismi-icerik: 0

KE21. Bu ölçümler async ile kurulan Deferred nesneleri için. launch ile kurulan Job nesnelerinin ve CompletableDeferred gibi doğrudan kurulan sözlerin eşitliği ölçülmedi.

Sınırlayıcı Ölçüm

Sonucu almak eşitliği değiştirir. Aşağıdaki blok üç şeyi yan yana ölçer: Deferred nesnelerinin kendisini, onların await() ile alınmış sonuçlarını ve aynı eş yordamın bağlamına konabilen iki CoroutineName("a") öğesini. Sonuçlar için 1000 seçildi; küçük tam sayılar kutulama sırasında paylaşılabilir ve tekil çıkabilir, 1000 için bu ayrıntıya bağlı kalınmaz, sonuç sandbox’ın bastığı olur.

// Main.kt — sonuc ve baglam ogesi
import kotlinx.coroutines.*

fun main() = runBlocking {
    val a = async { 1000 }
    val b = async { 1000 }
    val c = async { 2000 }
    val checks = listOf(
        Criterion("deferred", a, b, c),
        Criterion("sonuc", a.await(), b.await(), c.await()),
        Criterion("ad", CoroutineName("a"), CoroutineName("a"), CoroutineName("b")),
    )
    for (check in checks) check.report()
    summarize(checks)
}
deferred: ayni-nesne=false, ikiz=false, farkli=false -> kimlik
sonuc: ayni-nesne=false, ikiz=true, farkli=false -> icerik
ad: ayni-nesne=false, ikiz=true, farkli=false -> icerik
tekil: 0
kimlik: 1
icerik: 2
kismi-icerik: 0

Sınırlayıcı ölçüm şudur: eşitliği iş değil, işin sonucunu taşıyan tip belirler. Deferred kimlikle karşılaştırılır; await() ile alınan Int ise içerikle. Aynı a ve b işleri, bir kez nesne olarak, bir kez sonuç olarak ölçüldü ve ölçüt kimlikten icerike döndü. CoroutineName de içerikle karşılaştırılır: aynı adı taşıyan iki öğe eşittir, farklı adlı olan eşit değildir. Bu, eş yordam bağlamındaki bir öğenin kimlik değil değer taşıdığını gösterir.

Pratik çıkarım: iki işi karşılaştırmak gerekiyorsa Deferred’ları değil sonuçları karşılaştırmak gerekir; bunun için önce await() çağırmak gerekir. Bu çağrı da yalnız bir eş yordam içinde yapılabilir; await() bir askıya alınan fonksiyondur.

KE22. Bu ölçümler Int sonuçları ve CoroutineName öğeleri için. Başka sonuç tiplerinin eşitliği, Dispatchers öğelerinin eşitliği ve birden çok bağlam öğesinin birleşiminin eşitliği ölçülmedi.

Tembel Başlatma ve İptal

async ve launch varsayılan olarak hemen sıraya girer. start = CoroutineStart.LAZY verilirse eş yordam kurulur ama başlatılmaz: gövdesi, await(), join() ya da start() çağrılana kadar hiç çalışmaz. Bu, önceki dersteki tembel dizinin fikrine benzer, ama burada tembel olan bir hesabın sonucu değil, bir işin başlangıcıdır. İptal ise başlamamış bir işi hiç çalışmadan sonlandırabilir. Aşağıdaki blok önce tembel bir async kurar ve await() öncesinde, sonrasında ona ve listeye bakar. Sonra tembel bir launch kurar, iptal eder ve ancak ondan sonra bekler. Sıralar yalnız await() ve join() çağrılarının güvence verdiği noktalardan okunur.

// Main.kt — tembel baslatma ve iptal
import kotlinx.coroutines.*

fun main() = runBlocking {
    val log = mutableListOf<String>()
    val lazy = async(start = CoroutineStart.LAZY) { log.add("govde"); 7 }
    println(log.size)
    println(lazy.isActive)
    println(lazy.await())
    println(log)
    println(lazy.isCompleted)
    val cancelled = launch(start = CoroutineStart.LAZY) { log.add("kosmaz") }
    cancelled.cancel()
    cancelled.join()
    println(cancelled.isCancelled)
    println(log)
}
0
false
7
[govde]
true
true
[govde]

Tembel async kurulduğunda liste boştu ve iş etkin değildi: gövde await() çağrısına kadar hiç çalışmadı. await() gövdeyi başlattı, sonucu 7 olarak verdi ve listeye govde kaydı düştü. Ardından iş tamamlanmış sayıldı. Tembel launch ise iptal edildiği için gövdesine hiç girmedi: son satırda kosmaz kaydı yoktur, ama iş isCancelled olarak işaretlendi. Sonuç şudur: bir eş yordamın başlatılmış olması ile bitirilmiş olması farklı şeylerdir ve Job bu durumu sorgulatır. Sonucu hiç kullanılmayacak bir işi iptal etmek, onun gövdesinin yan etkilerini de engelleyebilir.

KE23. Bu ölçümler CoroutineStart.LAZY ile kurulmuş, runBlocking kapsamındaki işler için. Çalışmakta olan bir işin ortasında iptalin etkisi ve start() ile başlatmanın sırası ölçülmedi.

Bütün Çocukları Beklemek

Birden çok launch başlatıldığında hepsinin bitişini ayrı ayrı beklemek zorunlu değildir. coroutineScope { } bloğu, içinde başlatılan bütün işler bitmeden dönmez; joinAll() ise verilen Job listesindeki hepsini bekler. İki yolun da güvencesi aynıdır: dönüşten sonra işlerin yan etkileri görünürdür. Ama işlerin kendi arasındaki sırası bu güvencenin parçası değildir. Bu yüzden aşağıdaki blok sıralamayı değil, sonucu ölçer: dönüşten sonra listeyi sıralar ve kaç eleman bulunduğunu sayar.

// Main.kt — coroutineScope ve joinAll
import kotlinx.coroutines.*

fun main() = runBlocking {
    val log = mutableListOf<Int>()
    coroutineScope {
        for (i in 1..3) {
            launch {
                yield()
                log.add(i)
            }
        }
    }
    println(log.sorted())
    val jobs = List(4) { i ->
        launch {
            yield()
            log.add(i * 10)
        }
    }
    jobs.joinAll()
    println(log.size)
    println(jobs.all { it.isCompleted })
}
[1, 2, 3]
7
true

coroutineScope bloğu döndüğünde üç kayıt da listedeydi. Ölçüm sıralanmış listeyi bastı, çünkü işlerin hangi sırayla bittiği güvenceli değildir. İkinci kümede dört iş joinAll() ile beklendi ve listede toplam yedi eleman oldu; hepsi tamamlanmış göründü. Güvence ve sıra ayrımı pratikte önemlidir: bir sonucu beklemek için kapsam ya da join kullanılır, bitiş sırasına dayanan bir mantık ise yanlış kurulmuş olur.

KE24. Bu ölçümler launch işleri ve Int listesi için. Bir çocuk işin istisna fırlatması durumunda kapsamın ve kardeş işlerin davranışı bu derste ölçülmedi.

Bağlam ve Ad

Her eş yordam bir bağlam taşır ve bağlam öğeleri anahtarla okunur. coroutineContext[CoroutineName] varsa adı verir, yoksa null döner. withContext(CoroutineName("worker")) { } bloğu içinde bağlam yeni adı taşır ve blok bitince eski bağlam geri gelir. async ya da launch çağrısına bir bağlam öğesi verilirse, çocuk iş onu taşır. Aşağıdaki blok adı okuyan küçük bir askıya alınan fonksiyon tanımlar ve onu dört yerden çağırır. Ad bir iş parçacığı adı değildir; yalnız bağlama konmuş bir etikettir, bu yüzden ölçülebilir.

// Main.kt — baglamdan ad okumak
import kotlinx.coroutines.*
import kotlin.coroutines.coroutineContext

suspend fun currentName(): String? = coroutineContext[CoroutineName]?.name

fun main() = runBlocking {
    println(currentName())
    println(withContext(CoroutineName("worker")) { currentName() })
    println(currentName())
    val named = async(CoroutineName("child")) { currentName() }
    println(named.await())
}
null
worker
null
child

İlk çağrıda runBlocking bağlamında ad yoktu ve sonuç null. withContext bloğunda ad worker oldu. Blok bittikten sonra ad yine null: bağlam değişikliği bloğa özeldi. Son satırda çocuk async kendi adı olan child değerini okudu. Bağlam bu yüzden eş yordamlar arasında bilgi taşımanın temiz bir yoludur: parametre geçirmeden, ama kapsamı belli bir biçimde.

KE25. Bu ölçümler CoroutineName öğesi ve runBlocking kapsamı için. Job öğesinin ve Dispatchers öğesinin bağlamdan okunması, çocuğun üst bağlamdan öğe devralışı ve iki öğenin + ile birleşimi ölçülmedi.

İş Durumları ve Tekrarlı await

Her launch ve async bir Job taşır ve Job işin hangi aşamada olduğunu üç özellikle söyler: isActive işin sürdüğünü, isCompleted bittiğini, isCancelled iptal edildiğini gösterir. Bir işin bittiğini join() ya da await() döndükten sonra söylemek güvencelidir; başlatma ile ilk askıya alma noktası arasındaki ara durumlar ise dağıtıcıya bağlıdır ve bu derste ölçülmez. await() ayrıca birden çok kez çağrılabilir: sonuç bir kez hesaplanır ve sonraki çağrılar aynı değeri verir. Aşağıdaki blok önce join() sonrasındaki durumu, sonra aynı Deferred üzerinde iki await() çağrısını ve gövdenin kaç kez koştuğunu ölçer.

// Main.kt — is durumlari ve tekrarli await
import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch { yield() }
    job.join()
    println(job.isActive)
    println(job.isCompleted)
    println(job.isCancelled)
    var runs = 0
    val deferred = async { runs++; "sonuc" }
    println(deferred.await())
    println(deferred.await())
    println(runs)
}
false
true
false
sonuc
sonuc
1

join() döndükten sonra iş artık etkin değildi, tamamlanmıştı ve iptal edilmemişti. Deferred iki kez beklendi ve iki seferde de aynı değeri verdi; sayaç ise bir kez arttı, yani gövde yalnız bir kez koştu. await() işi yeniden başlatmaz, sonucu okur. Bunun bir anlamı da şudur: Deferred sonucu bir kez hesaplandıktan sonra saklar ve her await() o saklanan değeri okur. Bu, akıştaki durumun tersidir: soğuk akış her toplamada yeniden çalışır, Deferred ise tek hesabın sonucunu paylaşır.

KE26. Bu ölçümler join() ve await() sonrası durumlar için. Başlatma ile ilk askıya alma arasındaki isActive değeri ve hatayla biten işin durumu ölçülmedi.

Özet

  • suspend ile işaretlenen fonksiyon yalnız bir eş yordamdan ya da başka bir askıya alınan fonksiyondan çağrılır; launch sonuçsuz iş, async sonuçlu iş başlatır.
  • launch çağrısı hemen çalışmaz; runBlocking içinde başlatan eş yordam askıya alma noktasına gelene kadar bekler. join() ve await() işin bittiğini güvence eder.
  • İki ayrı async çağrısının Deferred nesneleri eşit değildir. Bu dersin ölçütü: kimlik.
  • Sınırlayıcı ölçüm: aynı işlerin await() sonuçları icerik ölçütünü verir; CoroutineName öğeleri de içerikle karşılaştırılır.

Sonraki Adım

Bu ders tek bir değer üreten işleri ele aldı. Bir iş tek değer yerine zamanla değer akıtabilir. Bir sonraki ders bu durumu akışlarla (flow) kurar ve eşitlik sorusunu orada da sorar: aynı değerleri akıtan iki akış eşit midir ve art arda gelen eşit değerler akışta nasıl ele alınır?

İ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