Kotlin: withContext () vs Async-await

91

Saya telah membaca dokumen kotlin , dan jika saya mengerti dengan benar, kedua fungsi Kotlin berfungsi sebagai berikut:

  1. withContext(context): mengganti konteks coroutine saat ini, ketika blok yang diberikan dijalankan, coroutine beralih kembali ke konteks sebelumnya.
  2. async(context): Memulai coroutine baru dalam konteks yang diberikan dan jika kita memanggil tugas yang .await()dikembalikan Deferred, ini akan menangguhkan pemanggilan coroutine dan melanjutkan ketika blok yang dieksekusi di dalam coroutine yang muncul kembali.

Sekarang untuk dua versi berikut code:

Versi 1:

  launch(){
    block1()
    val returned = async(context){
      block2()
    }.await()
    block3()
  }

Versi2:

  launch(){
    block1()
     val returned = withContext(context){
      block2()
    }
    block3()
  }
  1. Di kedua versi block1 (), block3 () dieksekusi dalam konteks default (commonpool?) Dimana block2 () dieksekusi dalam konteks yang diberikan.
  2. Eksekusi keseluruhan sinkron dengan urutan block1 () -> block2 () -> block3 ().
  3. Satu-satunya perbedaan yang saya lihat adalah bahwa versi1 membuat coroutine lain, sedangkan versi2 hanya mengeksekusi satu coroutine sambil mengganti konteks.

Pertanyaan saya adalah:

  1. Bukankah selalu lebih baik untuk digunakan withContextdaripada async-awaitkarena fungsinya mirip, tetapi tidak membuat coroutine lain. Coroutine dalam jumlah besar, meskipun ringan, masih bisa menjadi masalah dalam aplikasi yang menuntut.

  2. Apakah ada kasus async-awaityang lebih disukai withContext?

Pembaruan: Kotlin 1.2.50 sekarang memiliki pemeriksaan kode yang dapat dikonversi async(ctx) { }.await() to withContext(ctx) { }.

Mangat Rai Modi
sumber
Saya pikir ketika Anda menggunakan withContext, coroutine baru selalu dibuat apa pun. Inilah yang dapat saya lihat dari kode sumber.
stdout
@stdout Tidak async/awaitjuga membuat coroutine baru, menurut OP?
IgorGanapolsky

Jawaban:

126

Sejumlah besar coroutine, meski ringan, masih bisa menjadi masalah dalam aplikasi yang menuntut

Saya ingin menghilangkan mitos tentang "terlalu banyak coroutine" yang menjadi masalah dengan menghitung biaya sebenarnya.

Pertama, kita harus memisahkan coroutine itu sendiri dari konteks coroutine yang dilampirkan. Ini adalah cara Anda membuat coroutine dengan overhead minimum:

GlobalScope.launch(Dispatchers.Unconfined) {
    suspendCoroutine<Unit> {
        continuations.add(it)
    }
}

Nilai dari ekspresi ini adalah Jobholding coroutine yang ditangguhkan. Untuk mempertahankan kelanjutan, kami menambahkannya ke daftar dalam cakupan yang lebih luas.

Saya membandingkan kode ini dan menyimpulkan bahwa ia mengalokasikan 140 byte dan membutuhkan 100 nanodetik untuk menyelesaikannya. Jadi, begitulah ringannya coroutine.

Untuk reproduktifitas, ini adalah kode yang saya gunakan:

fun measureMemoryOfLaunch() {
    val continuations = ContinuationList()
    val jobs = (1..10_000).mapTo(JobList()) {
        GlobalScope.launch(Dispatchers.Unconfined) {
            suspendCoroutine<Unit> {
                continuations.add(it)
            }
        }
    }
    (1..500).forEach {
        Thread.sleep(1000)
        println(it)
    }
    println(jobs.onEach { it.cancel() }.filter { it.isActive})
}

class JobList : ArrayList<Job>()

class ContinuationList : ArrayList<Continuation<Unit>>()

Kode ini memulai sekumpulan coroutine dan kemudian tidur sehingga Anda punya waktu untuk menganalisis heap dengan alat pemantauan seperti VisualVM. Saya membuat kelas khusus JobListdan ContinuationListkarena ini membuatnya lebih mudah untuk menganalisis heap dump.


Untuk mendapatkan cerita yang lebih lengkap, saya menggunakan kode di bawah ini untuk mengukur juga biaya withContext()dan async-await:

import kotlinx.coroutines.*
import java.util.concurrent.Executors
import kotlin.coroutines.suspendCoroutine
import kotlin.system.measureTimeMillis

const val JOBS_PER_BATCH = 100_000

var blackHoleCount = 0
val threadPool = Executors.newSingleThreadExecutor()!!
val ThreadPool = threadPool.asCoroutineDispatcher()

fun main(args: Array<String>) {
    try {
        measure("just launch", justLaunch)
        measure("launch and withContext", launchAndWithContext)
        measure("launch and async", launchAndAsync)
        println("Black hole value: $blackHoleCount")
    } finally {
        threadPool.shutdown()
    }
}

fun measure(name: String, block: (Int) -> Job) {
    print("Measuring $name, warmup ")
    (1..1_000_000).forEach { block(it).cancel() }
    println("done.")
    System.gc()
    System.gc()
    val tookOnAverage = (1..20).map { _ ->
        System.gc()
        System.gc()
        var jobs: List<Job> = emptyList()
        measureTimeMillis {
            jobs = (1..JOBS_PER_BATCH).map(block)
        }.also { _ ->
            blackHoleCount += jobs.onEach { it.cancel() }.count()
        }
    }.average()
    println("$name took ${tookOnAverage * 1_000_000 / JOBS_PER_BATCH} nanoseconds")
}

fun measureMemory(name:String, block: (Int) -> Job) {
    println(name)
    val jobs = (1..JOBS_PER_BATCH).map(block)
    (1..500).forEach {
        Thread.sleep(1000)
        println(it)
    }
    println(jobs.onEach { it.cancel() }.filter { it.isActive})
}

val justLaunch: (i: Int) -> Job = {
    GlobalScope.launch(Dispatchers.Unconfined) {
        suspendCoroutine<Unit> {}
    }
}

val launchAndWithContext: (i: Int) -> Job = {
    GlobalScope.launch(Dispatchers.Unconfined) {
        withContext(ThreadPool) {
            suspendCoroutine<Unit> {}
        }
    }
}

val launchAndAsync: (i: Int) -> Job = {
    GlobalScope.launch(Dispatchers.Unconfined) {
        async(ThreadPool) {
            suspendCoroutine<Unit> {}
        }.await()
    }
}

Ini adalah keluaran khas yang saya dapatkan dari kode di atas:

Just launch: 140 nanoseconds
launch and withContext : 520 nanoseconds
launch and async-await: 1100 nanoseconds

Ya, async-awaitmembutuhkan waktu sekitar dua kali lebih lama withContext, tetapi itu masih hanya satu mikrodetik. Anda harus meluncurkannya dalam putaran yang ketat, hampir tidak melakukan apa pun selain itu, agar hal itu menjadi "masalah" di aplikasi Anda.

Menggunakan measureMemory()saya menemukan biaya memori berikut per panggilan:

Just launch: 88 bytes
withContext(): 512 bytes
async-await: 652 bytes

Biayanya async-awaittepat 140 byte lebih tinggi dari withContext, angka yang kami dapatkan sebagai bobot memori satu coroutine. Ini hanyalah sebagian kecil dari biaya lengkap untuk menyiapkan CommonPoolkonteks.

Jika pengaruh kinerja / memori adalah satu-satunya kriteria untuk memutuskan antara withContextdan async-await, kesimpulannya adalah bahwa tidak ada perbedaan yang relevan di antara keduanya dalam 99% kasus penggunaan nyata.

Alasan sebenarnya adalah withContext()API yang lebih sederhana dan lebih langsung, terutama dalam hal penanganan pengecualian:

  • Pengecualian yang tidak ditangani dalam async { ... }menyebabkan pekerjaan induknya dibatalkan. Ini terjadi terlepas dari bagaimana Anda menangani pengecualian dari pencocokan await(). Jika Anda belum menyiapkannya coroutineScope, ini dapat menurunkan seluruh aplikasi Anda.
  • Pengecualian yang tidak ditangani dalam withContext { ... }hanya dilemparkan oleh withContextpanggilan, Anda menanganinya seperti yang lain.

withContext juga kebetulan dioptimalkan, memanfaatkan fakta bahwa Anda menangguhkan coroutine induk dan menunggu turunannya, tetapi itu hanya bonus tambahan.

async-awaitharus disediakan untuk kasus-kasus di mana Anda benar-benar menginginkan konkurensi, sehingga Anda meluncurkan beberapa coroutine di latar belakang dan baru kemudian menunggunya. Pendeknya:

  • async-await-async-await - jangan lakukan itu, gunakan withContext-withContext
  • async-async-await-await - begitulah cara menggunakannya.
Marko Topolnik
sumber
Mengenai biaya memori tambahan async-await: Saat kami menggunakan withContext, coroutine baru juga dibuat (sejauh yang saya lihat dari kode sumber) jadi menurut Anda perbedaannya mungkin berasal dari tempat lain?
stdout
1
@stdout Perpustakaan telah berkembang sejak saya menjalankan tes ini. Kode dalam jawaban seharusnya sepenuhnya mandiri, coba jalankan lagi untuk memvalidasi. asyncmembuat Deferredobjek, yang mungkin juga menjelaskan beberapa perbedaannya.
Marko Topolnik
~ " Untuk mempertahankan kelanjutan ". Kapan kita perlu menyimpan ini?
IgorGanapolsky
1
@IgorGanapolsky Itu selalu dipertahankan tetapi biasanya tidak terlihat oleh pengguna. Kehilangan kelanjutan sama dengan Thread.destroy()- eksekusi menghilang ke udara tipis.
Marko Topolnik
22

Bukankah selalu lebih baik menggunakan withContext daripada asynch-await karena fungsinya mirip, tetapi tidak membuat coroutine lain. Coroutine numebr besar, meskipun ringan masih bisa menjadi masalah dalam aplikasi yang menuntut

Apakah ada kasus asynch-await lebih disukai daripada withContext

Anda harus menggunakan async / await saat ingin menjalankan beberapa tugas secara bersamaan, misalnya:

runBlocking {
    val deferredResults = arrayListOf<Deferred<String>>()

    deferredResults += async {
        delay(1, TimeUnit.SECONDS)
        "1"
    }

    deferredResults += async {
        delay(1, TimeUnit.SECONDS)
        "2"
    }

    deferredResults += async {
        delay(1, TimeUnit.SECONDS)
        "3"
    }

    //wait for all results (at this point tasks are running)
    val results = deferredResults.map { it.await() }
    println(results)
}

Jika Anda tidak perlu menjalankan banyak tugas secara bersamaan, Anda dapat menggunakan withContext.

Dmitry
sumber
13

Jika ragu, ingat ini seperti aturan praktis:

  1. Jika banyak tugas harus dilakukan secara paralel dan hasil akhir tergantung pada penyelesaian semuanya, maka gunakan async.

  2. Untuk mengembalikan hasil dari satu tugas, gunakan withContext.

Yogesh Umesh Vaity
sumber
1
Apakah keduanya asyncdan withContextmemblokir dalam lingkup penangguhan?
IgorGanapolsky
3
@IgorGanapolsky Jika Anda berbicara tentang pemblokiran utas utama, asyncdan withContexttidak akan memblokir utas utama, mereka hanya akan menangguhkan badan coroutine sementara beberapa tugas yang berjalan lama sedang berjalan dan menunggu hasilnya. Untuk info dan contoh selengkapnya, lihat artikel ini di Medium: Operasi Asinkron dengan Coroutines Kotlin .
Yogesh Umesh Vaity