Kotlin: withContext() vs Async-await

я kotlin docs и если я правильно понял, две функции Котлина работают следующим образом:

  1. withContext(context): переключает контекст текущей корутины, когда данный блок выполняется, корутина переключается обратно в предыдущий контекст.
  2. async(context): запускает новую корутину в данном контексте, и если мы вызовем await() на возвращенном Deferred задача, она приостанавливает вызывающую подпрограмму и возобновляется при выполнении блока внутри возвращается порожденная корутина.

теперь для следующих двух версий code :

Version1:

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

Version2:

  launch(){
    block1()
     val returned = withContext(context){
      block2()
    }
    block3()
  }
  1. в обеих версиях block1 (), block3 () выполняется в контексте по умолчанию(commonpool?) где as blocks() выполняется в данном контексте.
  2. общее выполнение синхронно с block1 () - > block2 () - > block3() порядок.
  3. единственная разница, которую я вижу, заключается в том, что version1 создает другую корутину, где как version2 выполняет только одну корутину при переключении контекста.

мои вопросы :

  1. разве не всегда лучше использовать withContext, а не asynch-await как это funcationally похожие, но не создать другой сопрограммы. Большие numebrs coroutines, хотя легкий вес все еще может быть проблемой в требовательных приложения

  2. есть ли случай asynch-await предпочтительнее withContext

обновление: Котлин 1.2.50 теперь имеет проверку кода, где он может конвертировать async(ctx) { }.await() to withContext(ctx) { }.

2 ответов


большое количество сопрограммы, хотя и легкий, все-таки может быть проблема в требовательных приложениях

я хотел бы развеять этот миф о "слишком много сопрограммы" проблемы путем количественной оценки их фактической стоимости.

во-первых, мы должны disentagle в coroutine С контекст coroutine, к которому он прикреплен. Вот как вы создаете только coroutine с минимумом накладные расходы:

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

значение этого выражения -Job проведение подвесной корутины. Чтобы сохранить продолжение, мы добавили его в более широкий список.

я проверил этот код и пришел к выводу, что он выделяет 140 байт и занимает 100 наносекунд завершить. Так вот насколько легок корутин.

для воспроизводимости, это код, который я использовал:

fun measureMemoryOfLaunch() {
    val continuations = ContinuationList()
    val jobs = (1..10_000).mapTo(JobList()) {
        launch(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.experimental.*
import java.util.concurrent.Executors
import kotlin.coroutines.experimental.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..10).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 = {
    launch(Unconfined) {
        suspendCoroutine<Unit> {}
    }
}

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

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

это типичный вывод, который я получаю из вышеуказанного кода:

Just launch: 93 nanoseconds
launch and withContext : 452 nanoseconds
launch and async-await: 897 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 должен быть зарезервирован для тех случаев, когда вы на самом деле хотите параллелизма, так что вы запустите несколько сопрограмм в фоновом режиме и лишь потом ждут на них. Короче:

  • async-await-async-await - аналогично withContext-withContext
  • async-async-await-await - вот как это использовать.

не всегда лучше использовать withContext, а не asynch-await, поскольку он функционально похож, но не создает другую корутину. Большие numebrs coroutines, хотя легкий вес все еще может быть проблемой в требовательных приложениях

есть ли случай, когда 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)
}

Если вы не необходимо запускать несколько задач одновременно, вы можете использовать withContext.