Kotlin: withContext() vs Async-wait - программирование
Подтвердить что ты не робот

Kotlin: withContext() vs Async-wait

Я был 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) { }.

4b9b3361

Ответ 1

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

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

Во-первых, мы должны распутать сам 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 занимает примерно в два раза больше, чем при withContext, но все равно всего лишь микросекунда. Вам придется запускать их в узком цикле, делая почти ничего, кроме того, чтобы это стало "проблемой" в вашем приложении.

Используя measureMemory() я нашел следующую стоимость памяти за звонок:

Just launch: 88 bytes
withContext(): 512 bytes
async-await: 652 bytes

Стоимость async-await в точности на 140 байтов выше, чем withContext, номер, который мы получили в качестве веса памяти одной сопрограммы. Это всего лишь часть полной стоимости настройки контекста CommonPool.

Если влияние производительности/памяти было единственным критерием для принятия решения между withContext и async-await withContext, то вывод должен заключаться в том, что нет никакой разницы между ними в 99% реальных случаев использования.

Настоящая причина заключается в том, что withContext() - более простой и более прямой API, особенно с точки зрения обработки исключений:

  • Исключение, которое не обрабатывается в async {... } заставляет его родительское задание отменяться. Это происходит независимо от того, как вы обрабатываете исключения из соответствия await(). Если вы не подготовили для него coroutineScope, это может привести к удалению всего вашего приложения.
  • Исключение, не обрабатываемое внутри withContext {... } просто получает вызов withContext, вы обрабатываете его так же, как и любой другой.

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

async-await должен быть зарезервирован для тех случаев, когда вы действительно хотите параллелизма, чтобы вы запускали несколько сопрограмм в фоновом режиме и только после этого ожидали их. Короче:

  • async-await-async-await withContext-withContext - то же самое, что и с withContext-withContext
  • async-async-await-await - это способ его использования.

Ответ 2

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

Есть ли случай, когда asynch-wait более предпочтительнее withContext

Вы должны использовать async/wait, когда вы хотите выполнять несколько задач одновременно, например:

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.

Ответ 3

Если вы сомневаетесь, запомните это, как правило:

  1. Если несколько задач должны выполняться параллельно, и конечный результат зависит от выполнения всех из них, используйте async.

  2. Для возврата результата одной задачи используйте withContext.