Kotlin: withContext () เทียบกับ Async-await


93

ฉันอ่านเอกสาร kotlinแล้วและถ้าฉันเข้าใจอย่างถูกต้องฟังก์ชัน Kotlin ทั้งสองจะทำงานดังนี้:

  1. withContext(context): สลับบริบทของโครูทีนปัจจุบันเมื่อบล็อกที่กำหนดดำเนินการโครูทีนจะสลับกลับไปยังบริบทก่อนหน้า
  2. async(context): เริ่มโครูทีนใหม่ในบริบทที่กำหนดและหากเราเรียก.await()ใช้Deferredงานที่ส่งคืนมันจะระงับการเรียกโครูทีนและกลับมาทำงานต่อเมื่อบล็อกที่ดำเนินการภายในโครูทีนที่ถูกสร้างจะส่งกลับ

ตอนนี้สำหรับสองเวอร์ชันต่อไปนี้code:

เวอร์ชัน 1:

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

เวอร์ชัน 2:

  launch(){
    block1()
     val returned = withContext(context){
      block2()
    }
    block3()
  }
  1. ในทั้งสองเวอร์ชัน block1 (), block3 () ดำเนินการในบริบทเริ่มต้น (commonpool?) โดยที่ block2 () ดำเนินการในบริบทที่กำหนด
  2. การดำเนินการโดยรวมเป็นแบบซิงโครนัสกับคำสั่ง block1 () -> block2 () -> block3 ()
  3. ความแตกต่างเพียงอย่างเดียวที่ฉันเห็นคือเวอร์ชัน 1 สร้างโครูทีนอื่นโดยที่เวอร์ชัน 2 รันโครูทีนเพียงตัวเดียวในขณะที่เปลี่ยนบริบท

คำถามของฉันคือ:

  1. มันไม่ดีกว่าที่จะใช้เสมอไปwithContextแทนที่จะใช้งานasync-awaitได้เหมือนกัน แต่ไม่ได้สร้างโครูทีนอื่น โครูทีนจำนวนมากแม้ว่าจะมีน้ำหนักเบา แต่ก็ยังคงเป็นปัญหาในการใช้งานที่ต้องการ

  2. มีเคสasync-awaitไหนที่ชอบกว่ากันwithContext?

อัปเดต: Kotlin 1.2.50ขณะนี้มีการตรวจสอบรหัสที่สามารถแปลงasync(ctx) { }.await() to withContext(ctx) { }ได้


ฉันคิดว่าเมื่อคุณใช้โคwithContextรูทีนใหม่จะถูกสร้างขึ้นเสมอโดยไม่คำนึงถึง นี่คือสิ่งที่ฉันเห็นได้จากซอร์สโค้ด
stdout

@stdout async/awaitยังไม่สร้างโครูทีนใหม่ตาม OP?
IgorGanapolsky

คำตอบ:


128

โครูทีนจำนวนมากแม้ว่าจะมีน้ำหนักเบา แต่ก็ยังคงเป็นปัญหาในการใช้งานที่ต้องการ

ฉันต้องการขจัดความเชื่อที่ว่า "โครูทีนมากเกินไป" ซึ่งเป็นปัญหาโดยการหาปริมาณต้นทุนจริง

ครั้งแรกที่เราควรจะคลี่คลาย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 นั่นคือวิธีที่จะใช้

เกี่ยวกับต้นทุนหน่วยความจำเพิ่มเติมของasync-await: เมื่อเราใช้โคwithContextรูทีนใหม่จะถูกสร้างขึ้นด้วย (เท่าที่ฉันเห็นจากซอร์สโค้ด) คุณคิดว่าความแตกต่างอาจมาจากที่อื่นหรือไม่?
stdout

1
@stdout ไลบรารีได้รับการพัฒนาตั้งแต่ฉันทำการทดสอบเหล่านี้ รหัสในคำตอบควรมีอยู่ในตัวเต็มลองรันอีกครั้งเพื่อตรวจสอบความถูกต้อง asyncสร้างDeferredวัตถุซึ่งอาจอธิบายความแตกต่างบางอย่างได้
Marko Topolnik

~ " เพื่อรักษาความต่อเนื่อง ". เราต้องรักษาสิ่งนี้ไว้เมื่อใด
IgorGanapolsky

1
@IgorGanapolsky มันถูกเก็บไว้เสมอ แต่โดยปกติจะไม่ปรากฏให้ผู้ใช้เห็น การสูญเสียความต่อเนื่องนั้นเทียบเท่ากับThread.destroy()- การประหารที่หายไปในอากาศเบาบาง
Marko Topolnik

24

ไม่ดีกว่าเสมอไปที่จะใช้กับบริบทแทนที่จะเป็น asynch-await เพราะมันคล้ายกัน แต่ไม่ได้สร้างโครูทีนอื่น โครูทีนตัวเลขขนาดใหญ่แม้ว่าน้ำหนักเบาอาจเป็นปัญหาในการใช้งานที่ต้องการ

มีกรณี asynch-await หรือไม่ที่เป็นที่นิยมกว่ากับ withContext

คุณควรใช้ async / await เมื่อคุณต้องการดำเนินการหลายงานพร้อมกันตัวอย่างเช่น:

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)
}

หากคุณไม่จำเป็นต้องรันหลาย ๆ งานพร้อมกันคุณสามารถใช้กับบริบท


15

เมื่อมีข้อสงสัยโปรดจำสิ่งนี้ไว้เป็นหลัก:

  1. asyncถ้าหลายงานที่ต้องเกิดขึ้นในแบบคู่ขนานและผลสุดท้ายขึ้นอยู่กับความสำเร็จของพวกเขาทั้งหมดแล้วใช้

  2. withContextสำหรับการกลับมาของผลงานเดียวใช้


1
ทั้งสองอย่างasyncและwithContextการบล็อกอยู่ในขอบเขตการระงับหรือไม่?
IgorGanapolsky

3
@IgorGanapolsky หากคุณกำลังพูดถึงการบล็อกเธรดหลักasyncและwithContextจะไม่ปิดกั้นเธรดหลักพวกเขาจะระงับเฉพาะเนื้อหาของโครูทีนในขณะที่งานที่รันเป็นเวลานานกำลังทำงานอยู่และรอผลลัพธ์ สำหรับข้อมูลเพิ่มเติมและตัวอย่างโปรดดูบทความนี้ในกลาง: Async ดำเนินงานที่มี Kotlin Coroutines
Yogesh Umesh Vaity
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.