Çok sayıda koroutin, hafif olmasına rağmen, zorlu uygulamalarda yine de sorun olabilir
Gerçek maliyetlerini ölçerek bu "çok fazla eşgüdüm" sorununun bir sorun olduğu efsanesini ortadan kaldırmak istiyorum.
İlk olarak, koroutinin kendisini bağlı olduğu koroutin bağlamından ayırmalıyız . Minimum ek yük ile sadece bir korutin oluşturmanın yolu:
GlobalScope.launch(Dispatchers.Unconfined) {
suspendCoroutine<Unit> {
continuations.add(it)
}
}
Bu ifadenin değeri Job
askıya alınmış bir korutindir. Devamını korumak için daha geniş kapsamda bir listeye ekledik.
Bu kodu karşılaştırdım ve 140 bayt ayırdığı ve tamamlanması 100 nanosaniye sürdüğü sonucuna vardım . Demek bir koroutin bu kadar hafiftir.
Tekrarlanabilirlik için kullandığım kod bu:
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>>()
Bu kod, bir grup koroutini başlatır ve sonra uyur, böylece yığını VisualVM gibi bir izleme aracıyla analiz etmek için zamanınız olur. Özel sınıfları yarattım JobList
ve ContinuationList
bu, yığın dökümünü analiz etmeyi kolaylaştırdığı için.
Daha kapsamlı bir şekilde elde etmek için, ben de maliyetini ölçmek için aşağıdaki kodu kullanılır withContext()
ve 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()
}
}
Yukarıdaki koddan aldığım tipik çıktı bu:
Just launch: 140 nanoseconds
launch and withContext : 520 nanoseconds
launch and async-await: 1100 nanoseconds
Evet, async-await
iki kat daha uzun sürer withContext
, ancak yine de sadece bir mikrosaniye. Bunları sıkı bir döngü içinde başlatmanız ve bunun dışında neredeyse hiçbir şey yapmamanız gerekir, çünkü bunun uygulamanızda "sorun" haline gelmesi gerekir.
Kullanarak measureMemory()
arama başına aşağıdaki bellek ücretini buldum:
Just launch: 88 bytes
withContext(): 512 bytes
async-await: 652 bytes
Maliyeti , bir koroutinin hafıza ağırlığı olarak aldığımız sayıdan async-await
tam olarak 140 bayt daha yüksektir withContext
. Bu, CommonPool
bağlamı kurmanın tam maliyetinin sadece bir kısmıdır .
Performans / bellek etkisi withContext
ve arasında karar vermek için tek kriter async-await
olsaydı, sonuç, gerçek kullanım durumlarının% 99'unda aralarında anlamlı bir fark olmadığı olurdu.
Gerçek neden withContext()
, özellikle istisna işleme açısından daha basit ve daha doğrudan bir API olmasıdır:
- İçinde ele alınmayan bir istisna
async { ... }
, üst işinin iptal edilmesine neden olur. Bu, eşleşmedeki istisnaları nasıl ele aldığınıza bakılmaksızın gerçekleşir await()
. coroutineScope
Bunun için bir hazırlık yapmadıysanız, tüm başvurunuzu kapatabilir.
- İçinde ele alınmayan bir istisna
withContext { ... }
, withContext
çağrı tarafından atılır , siz onu tıpkı diğerleri gibi halledersiniz.
withContext
ayrıca ebeveyn coroutine'i askıya aldığınız ve çocuğu beklediğiniz gerçeğinden yararlanarak optimize edilir, ancak bu sadece ek bir bonus.
async-await
Gerçekten eşzamanlılık istediğiniz durumlar için ayrılmalıdır, böylece arka planda birkaç koroutin başlatır ve ancak o zaman onları beklersiniz. Kısacası:
async-await-async-await
- bunu yapma, kullan withContext-withContext
async-async-await-await
- onu kullanmanın yolu bu.
withContext
, ne olursa olsun her zaman yeni bir koroutin oluşturulur. Kaynak kodundan görebildiğim bu.