โครูทีนจำนวนมากแม้ว่าจะมีน้ำหนักเบา แต่ก็ยังคงเป็นปัญหาในการใช้งานที่ต้องการ
ฉันต้องการขจัดความเชื่อที่ว่า "โครูทีนมากเกินไป" ซึ่งเป็นปัญหาโดยการหาปริมาณต้นทุนจริง
ครั้งแรกที่เราควรจะคลี่คลาย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รูทีนใหม่จะถูกสร้างขึ้นเสมอโดยไม่คำนึงถึง นี่คือสิ่งที่ฉันเห็นได้จากซอร์สโค้ด