Котлін: withContext () проти Async-await


91

Я читав документи 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?
ІгорГанапольський

Відповіді:


126

Велика кількість програм, хоч і легка, все одно може бути проблемою у складних програмах

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

По- перше, ми повинні розплутати співпрограми безпосередньо від сопрограммное контексту , до якого він прикріплений. Ось як ви створюєте лише спільну програму з мінімальними накладними витратами:

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об'єкт, який може також пояснити деяку різницю.
Марко Топольник

~ " Зберегти продовження ". Коли нам потрібно зберегти це?
ІгорГанапольський

1
@IgorGanapolsky Він завжди зберігається, але зазвичай не таким чином, щоб він був видимим для користувача. Втрата продовження рівнозначна Thread.destroy()- виконанню, що зникає в повітрі.
Марко Топольник

22

Чи не завжди краще використовувати 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.


13

Якщо ви сумніваєтеся, пам’ятайте про це як про правило:

  1. Якщо кілька завдань повинні відбуватися паралельно, а кінцевий результат залежить від виконання всіх, то використовуйте async.

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


1
Чи є обидва asyncі withContextблокування в області призупинення?
ІгорГанапольський

3
@IgorGanapolsky Якщо ви говорите про блокування основного потоку asyncі withContextне заблокуєте основний потік, вони лише призупинять тіло програми, поки виконується якесь тривале завдання і чекає результату. Для отримання додаткової інформації та прикладу дивіться цю статтю про Medium: Async Operations with Kotlin Coroutines .
Йогеш Умеш Вайти
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.