Котлин: withContext () против Async-await - PullRequest
0 голосов
/ 08 мая 2018

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

  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?), Где block () выполняется в данном контексте.
  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) { }.

Ответы [ 2 ]

0 голосов
/ 08 мая 2018

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

Я бы хотел развеять этот миф о том, что «слишком много сопрограмм» является проблемой, количественно определив их фактическую стоимость.

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

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 & mdash; такой же как withContext-withContext
  • async-async-await-await & mdash; это способ использовать его.
0 голосов
/ 08 мая 2018

Разве не всегда лучше использовать withContext, чем 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)
}

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

...