बड़ी संख्या में coroutines, हालांकि हल्के, अभी भी मांग अनुप्रयोगों में एक समस्या हो सकती है
मैं "बहुत सारे कॉरआउट" के इस मिथक को दूर करना चाहता हूं, जो उनकी वास्तविक लागत को निर्धारित करके एक समस्या है।
सबसे पहले, हमें कोरटाइन को कोरटाइन संदर्भ से अलग करना चाहिए, जिससे यह जुड़ा हुआ है। इस तरह से आप न्यूनतम ओवरहेड के साथ सिर्फ एक कोरआउट बनाते हैं:
GlobalScope.launch(Dispatchers.Unconfined) {
suspendCoroutine<Unit> {
continuations.add(it)
}
}
इस अभिव्यक्ति का मूल्य Job
एक निलंबित कोरआउट है। निरंतरता बनाए रखने के लिए, हमने इसे व्यापक दायरे में एक सूची में जोड़ा।
मैंने इस कोड को बेंचमार्क किया और निष्कर्ष निकाला कि यह 140 बाइट्स आवंटित करता है और पूरा करने के लिए 100 नैनोसेकंड लेता है । तो यह है कि एक coroutine कितना हल्का है।
प्रतिलिपि प्रस्तुत करने योग्यता के लिए, यह वह कोड है जिसका मैंने उपयोग किया है:
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>>()
यह कोड कोरआउट्स का एक समूह शुरू करता है और फिर सोता है ताकि आपके पास विजुअल वीएम जैसे निगरानी उपकरण के साथ ढेर का विश्लेषण करने का समय हो। मैंने विशेष कक्षाएं बनाईं 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()
एक सरल और अधिक प्रत्यक्ष एपीआई, विशेष रूप से अपवाद से निपटने के संदर्भ में:
- एक अपवाद जो इसके हाथ में नहीं है,
async { ... }
उसके मूल कार्य को रद्द करने का कारण बनता है। ऐसा तब होता है जब आप मिलान से अपवादों को संभालते हैं await()
। यदि आपने इसके coroutineScope
लिए तैयारी नहीं की है, तो यह आपके पूरे आवेदन को नीचे ला सकता है।
withContext { ... }
केवल withContext
कॉल द्वारा फेंका गया अपवाद नहीं , आप इसे किसी अन्य की तरह संभालते हैं।
withContext
इस तथ्य को भी अनुकूलित किया जाना चाहिए, इस तथ्य का लाभ उठाते हुए कि आप माता-पिता की कोरटाउट को निलंबित कर रहे हैं और बच्चे पर इंतजार कर रहे हैं, लेकिन यह सिर्फ एक अतिरिक्त बोनस है।
async-await
उन मामलों के लिए आरक्षित किया जाना चाहिए जहां आप वास्तव में संगामिति चाहते हैं, ताकि आप पृष्ठभूमि में कई कोरआउट शुरू करें और उसके बाद ही उन पर प्रतीक्षा करें। संक्षेप में:
async-await-async-await
- ऐसा मत करो, का उपयोग करें withContext-withContext
async-async-await-await
- इसका उपयोग करने का यही तरीका है।
withContext
, तो एक नया कोरआउट हमेशा परवाह किए बिना बनाया जाता है। यह वही है जो मैं स्रोत कोड से देख सकता हूं।