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