โครูทีนจำนวนมากแม้ว่าจะมีน้ำหนักเบา แต่ก็ยังคงเป็นปัญหาในการใช้งานที่ต้องการ
ฉันต้องการขจัดความเชื่อที่ว่า "โครูทีนมากเกินไป" ซึ่งเป็นปัญหาโดยการหาปริมาณต้นทุนจริง
ครั้งแรกที่เราควรจะคลี่คลายcoroutineตัวเองจากบริบท coroutineที่มีการแนบ นี่คือวิธีสร้างโครูทีนโดยมีค่าโสหุ้ยต่ำสุด:
GlobalScope.launch(Dispatchers.Unconfined) {
suspendCoroutine<Unit> {
continuations.add(it)
}
}
ค่าของนิพจน์นี้คือ Job
ถือโครูทีนที่ถูกระงับ เพื่อรักษาความต่อเนื่องเราได้เพิ่มลงในรายการในขอบเขตที่กว้างขึ้น
ฉันเปรียบเทียบโค้ดนี้และสรุปว่าจัดสรร140 ไบต์และใช้เวลา100 นาโนวินาทีในการทำให้เสร็จสมบูรณ์ นั่นคือความเบาของโครูทีน
สำหรับการทำซ้ำนี่คือรหัสที่ฉันใช้:
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>>()
รหัสนี้เริ่มต้นโครูทีนจำนวนมากจากนั้นจึงเข้าสู่โหมดสลีปเพื่อให้คุณมีเวลาวิเคราะห์ฮีปด้วยเครื่องมือตรวจสอบเช่น VisualVM ฉันสร้างคลาสพิเศษขึ้นมาJobList
และด้วยContinuationList
เหตุนี้จึงช่วยให้วิเคราะห์ฮีปดัมพ์ได้ง่ายขึ้น
เพื่อให้ได้เรื่องราวที่สมบูรณ์ยิ่งขึ้นฉันใช้รหัสด้านล่างเพื่อวัดต้นทุนwithContext()
และ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()
}
}
นี่คือผลลัพธ์ทั่วไปที่ฉันได้รับจากโค้ดด้านบน:
Just launch: 140 nanoseconds
launch and withContext : 520 nanoseconds
launch and async-await: 1100 nanoseconds
ใช่async-await
ใช้เวลาประมาณสองเท่าwithContext
แต่ก็ยังคงเป็นเพียงไมโครวินาที คุณจะต้องเปิดใช้งานอย่างต่อเนื่องโดยแทบจะไม่ทำอะไรเลยนอกจากนี้เพื่อให้กลายเป็น "ปัญหา" ในแอปของคุณ
การใช้measureMemory()
ฉันพบต้นทุนหน่วยความจำต่อการโทร:
Just launch: 88 bytes
withContext(): 512 bytes
async-await: 652 bytes
ค่าใช้จ่ายasync-await
สูงกว่า 140 ไบต์withContext
โดยตัวเลขที่เราได้รับเป็นน้ำหนักหน่วยความจำของโครูทีน นี่เป็นเพียงส่วนหนึ่งของต้นทุนทั้งหมดในการตั้งค่าไฟล์CommonPool
บริบท
หากผลกระทบด้านประสิทธิภาพ / หน่วยความจำเป็นเกณฑ์เดียวในการตัดสินระหว่างwithContext
และasync-await
ข้อสรุปจะต้องเป็นว่าไม่มีความแตกต่างที่เกี่ยวข้องใน 99% ของกรณีการใช้งานจริง
เหตุผลที่แท้จริงคือwithContext()
API ที่ง่ายกว่าและตรงกว่าโดยเฉพาะในแง่ของการจัดการข้อยกเว้น:
- ข้อยกเว้นที่ไม่ได้รับการจัดการภายใน
async { ... }
ทำให้งานหลักถูกยกเลิก สิ่งนี้เกิดขึ้นไม่ว่าคุณจะจัดการกับข้อยกเว้นจากการจับคู่await()
อย่างไร หากคุณยังไม่ได้เตรียมการcoroutineScope
มาอาจทำให้แอปพลิเคชันทั้งหมดของคุณพังได้
- ข้อยกเว้นที่ไม่ได้รับการจัดการภายใน
withContext { ... }
เพียงแค่ถูกwithContext
โทรหาคุณก็จัดการได้เหมือนกับที่อื่น ๆ
withContext
นอกจากนี้ยังมีการปรับให้เหมาะสมโดยใช้ประโยชน์จากข้อเท็จจริงที่ว่าคุณกำลังระงับโครูทีนของผู้ปกครองและรอคอยเด็ก แต่นั่นเป็นเพียงโบนัสเพิ่มเติม
async-await
ควรสงวนไว้สำหรับกรณีที่คุณต้องการการทำงานพร้อมกันเพื่อให้คุณเปิดโครูทีนหลายตัวในพื้นหลังจากนั้นจึงรอเพียง ในระยะสั้น:
async-await-async-await
- อย่าทำอย่างนั้นใช้ withContext-withContext
async-async-await-await
นั่นคือวิธีที่จะใช้
withContext
รูทีนใหม่จะถูกสร้างขึ้นเสมอโดยไม่คำนึงถึง นี่คือสิ่งที่ฉันเห็นได้จากซอร์สโค้ด