Велика кількість програм, хоч і легка, все одно може бути проблемою у складних програмах
Я хотів би розвіяти цей міф про те, що "занадто багато спільних програм" є проблемою, визначивши їх фактичну вартість.
По- перше, ми повинні розплутати співпрограми безпосередньо від сопрограммное контексту , до якого він прикріплений. Ось як ви створюєте лише спільну програму з мінімальними накладними витратами:
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 - це спосіб його використовувати.
withContext, завжди створюється нова програма незалежно. Це те, що я бачу з вихідного коду.