Saya telah membaca dokumen kotlin , dan jika saya mengerti dengan benar, kedua fungsi Kotlin berfungsi sebagai berikut:
withContext(context)
: mengganti konteks coroutine saat ini, ketika blok yang diberikan dijalankan, coroutine beralih kembali ke konteks sebelumnya.async(context)
: Memulai coroutine baru dalam konteks yang diberikan dan jika kita memanggil tugas yang.await()
dikembalikanDeferred
, 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()
}
- Di kedua versi block1 (), block3 () dieksekusi dalam konteks default (commonpool?) Dimana block2 () dieksekusi dalam konteks yang diberikan.
- Eksekusi keseluruhan sinkron dengan urutan block1 () -> block2 () -> block3 ().
- Satu-satunya perbedaan yang saya lihat adalah bahwa versi1 membuat coroutine lain, sedangkan versi2 hanya mengeksekusi satu coroutine sambil mengganti konteks.
Pertanyaan saya adalah:
Bukankah selalu lebih baik untuk digunakan
withContext
daripadaasync-await
karena fungsinya mirip, tetapi tidak membuat coroutine lain. Coroutine dalam jumlah besar, meskipun ringan, masih bisa menjadi masalah dalam aplikasi yang menuntut.Apakah ada kasus
async-await
yang lebih disukaiwithContext
?
Pembaruan:
Kotlin 1.2.50 sekarang memiliki pemeriksaan kode yang dapat dikonversi async(ctx) { }.await() to withContext(ctx) { }
.
kotlin
kotlin-coroutines
Mangat Rai Modi
sumber
sumber
withContext
, coroutine baru selalu dibuat apa pun. Inilah yang dapat saya lihat dari kode sumber.async/await
juga membuat coroutine baru, menurut OP?Jawaban:
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
Job
holding 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
JobList
danContinuationList
karena 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()
danasync-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-await
membutuhkan waktu sekitar dua kali lebih lamawithContext
, 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-await
tepat 140 byte lebih tinggi dariwithContext
, angka yang kami dapatkan sebagai bobot memori satu coroutine. Ini hanyalah sebagian kecil dari biaya lengkap untuk menyiapkanCommonPool
konteks.Jika pengaruh kinerja / memori adalah satu-satunya kriteria untuk memutuskan antara
withContext
danasync-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:async { ... }
menyebabkan pekerjaan induknya dibatalkan. Ini terjadi terlepas dari bagaimana Anda menangani pengecualian dari pencocokanawait()
. Jika Anda belum menyiapkannyacoroutineScope
, ini dapat menurunkan seluruh aplikasi Anda.withContext { ... }
hanya dilemparkan olehwithContext
panggilan, 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-await
harus 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, gunakanwithContext-withContext
async-async-await-await
- begitulah cara menggunakannya.sumber
async-await
: Saat kami menggunakanwithContext
, coroutine baru juga dibuat (sejauh yang saya lihat dari kode sumber) jadi menurut Anda perbedaannya mungkin berasal dari tempat lain?async
membuatDeferred
objek, yang mungkin juga menjelaskan beberapa perbedaannya.Thread.destroy()
- eksekusi menghilang ke udara tipis.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.
sumber
Jika ragu, ingat ini seperti aturan praktis:
Jika banyak tugas harus dilakukan secara paralel dan hasil akhir tergantung pada penyelesaian semuanya, maka gunakan
async
.Untuk mengembalikan hasil dari satu tugas, gunakan
withContext
.sumber
async
danwithContext
memblokir dalam lingkup penangguhan?async
danwithContext
tidak 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 .