Kotlin: withContext() vs Async-await
я kotlin docs и если я правильно понял, две функции Котлина работают следующим образом:
-
withContext(context)
: переключает контекст текущей корутины, когда данный блок выполняется, корутина переключается обратно в предыдущий контекст. -
async(context)
: запускает новую корутину в данном контексте, и если мы вызовемawait()
на возвращенномDeferred
задача, она приостанавливает вызывающую подпрограмму и возобновляется при выполнении блока внутри возвращается порожденная корутина.
теперь для следующих двух версий code
:
Version1:
launch(){
block1()
val returned = async(context){
block2()
}.await()
block3()
}
Version2:
launch(){
block1()
val returned = withContext(context){
block2()
}
block3()
}
- в обеих версиях block1 (), block3 () выполняется в контексте по умолчанию(commonpool?) где as blocks() выполняется в данном контексте.
- общее выполнение синхронно с block1 () - > block2 () - > block3() порядок.
- единственная разница, которую я вижу, заключается в том, что version1 создает другую корутину, где как version2 выполняет только одну корутину при переключении контекста.
мои вопросы :
разве не всегда лучше использовать
withContext
, а неasynch-await
как это funcationally похожие, но не создать другой сопрограммы. Большие numebrs coroutines, хотя легкий вес все еще может быть проблемой в требовательных приложенияесть ли случай
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.