Kotlin: withContext () बनाम Async-wait


93

मैं कोटलिन डॉक्स पढ़ रहा हूं , और अगर मैं सही ढंग से समझ गया कि दो कोटलिन फ़ंक्शन निम्नानुसार काम करते हैं:

  1. withContext(context): जब वर्तमान ब्लॉक निष्पादित होता है, तो कोरटाइन का संदर्भ बदल जाता है, कोरटाइन पिछले संदर्भ में वापस आ जाता है।
  2. async(context): दिए गए संदर्भ में एक नया कॉरआउट शुरू करता है और अगर हम .await()लौटे हुए Deferredकार्य पर कॉल करते हैं , तो यह कॉलिंग कॉरआउट को फिर से शुरू कर देगा और जब ब्लॉक किए गए कोराउटीन रिटर्न के अंदर निष्पादित ब्लॉक को फिर से शुरू करेगा।

अब निम्नलिखित दो संस्करणों के लिए code:

संस्करण 1:

  launch(){
    block1()
    val returned = async(context){
      block2()
    }.await()
    block3()
  }

संस्करण 2:

  launch(){
    block1()
     val returned = withContext(context){
      block2()
    }
    block3()
  }
  1. दोनों संस्करणों में ब्लॉक 1 (), ब्लॉक 3 () डिफ़ॉल्ट संदर्भ में निष्पादित करें (कॉमनपूल?) जहां ब्लॉक 2 () दिए गए संदर्भ में निष्पादित होता है।
  2. समग्र निष्पादन ब्लॉक 1 () -> ब्लॉक 2 () -> ब्लॉक 3 () आदेश के साथ समकालिक है।
  3. मुझे केवल इतना अंतर दिखाई देता है कि संस्करण 1 एक और कोरआउट बनाता है, जहां संस्करण 2 संदर्भ स्विच करते समय केवल एक कोरआउट करता है।

मेरे प्रश्न हैं:

  1. यह हमेशा के withContextबजाय उपयोग करने के लिए बेहतर नहीं है async-awaitक्योंकि यह कार्यात्मक रूप से समान है, लेकिन एक और coroutine नहीं बनाता है। बड़ी संख्या में कोरआउट, हालांकि हल्के, अभी भी मांग अनुप्रयोगों में एक समस्या हो सकती है।

  2. क्या कोई मामला async-awaitअधिक बेहतर है withContext?

अपडेट: कोटलिन 1.2.50 में अब एक कोड निरीक्षण है जहां यह परिवर्तित हो सकता है async(ctx) { }.await() to withContext(ctx) { }


मुझे लगता है कि जब आप उपयोग करते हैं withContext, तो एक नया कोरआउट हमेशा परवाह किए बिना बनाया जाता है। यह वही है जो मैं स्रोत कोड से देख सकता हूं।
stdout

@stdout async/awaitओपी के अनुसार एक नया कोरआउट नहीं बना सकता है?
इगोरगानापोलस्की

जवाबों:


128

बड़ी संख्या में 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 - इसका उपयोग करने का यही तरीका है।

अतिरिक्त स्मृति लागत के बारे में async-await: जब हम उपयोग करते हैं withContext, तो एक नया कोरआउट भी बनाया जाता है (जहां तक ​​मैं स्रोत कोड से देख सकता हूं) तो आपको लगता है कि अंतर कहीं और से आ रहा होगा?
stdout

1
@stdout जब से मैं इन परीक्षणों को चला रहा हूँ पुस्तकालय विकसित हो रहा है। उत्तर में कोड को पूरी तरह से स्व-निहित माना जाता है, इसे फिर से सत्यापित करने के लिए चलाने का प्रयास करें। asyncएक Deferredवस्तु बनाता है , जो कुछ अंतर को भी समझा सकता है।
मार्को टोपोलनिक

~ " निरंतरता बनाए रखने के लिए "। हमें इसे बनाए रखने की आवश्यकता कब है?
इगोरगानापोलस्की

1
@IgorGanapolsky इसे हमेशा बनाए रखा जाता है, लेकिन आमतौर पर उपयोगकर्ता को दिखाई नहीं देता है। निरंतरता खोना समान है Thread.destroy()- निष्पादन पतली हवा में लुप्त हो जाना।
Marko Topolnik

24

क्या यह हमेशा asynch- वेट के बजाय कॉन्टेक्स्ट के साथ उपयोग करना बेहतर नहीं है क्योंकि यह मज़ेदार रूप से समान है, लेकिन एक और coroutine नहीं बनाता है। बड़े सुमेरु के कोरटाइन, हालांकि हल्के मांग वाले अनुप्रयोगों में अभी भी समस्या हो सकती है

वहाँ एक मामला है asynch- प्रतीक्षित withContext के लिए अधिक बेहतर है

जब आप कई कार्यों को समवर्ती रूप से निष्पादित करना चाहते हैं तो आपको async / प्रतीक्षा का उपयोग करना चाहिए, उदाहरण के लिए:

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)
}

यदि आपको कई कार्यों को समवर्ती रूप से चलाने की आवश्यकता नहीं है, तो आप कॉन्टेक्स्ट का उपयोग कर सकते हैं।


15

जब संदेह हो, तो इसे अंगूठे के नियम की तरह याद रखें:

  1. यदि कई कार्यों को समानांतर और अंतिम परिणाम में होना है, तो उन सभी को पूरा करने पर निर्भर करता है, फिर उपयोग करें async

  2. किसी एक कार्य के परिणाम को वापस करने के लिए, का उपयोग करें withContext


1
दोनों asyncऔर withContextएक को निलंबित दायरे में अवरुद्ध?
इगोरगानापोलस्की 17

3
@IgorGanapolsky यदि आप मुख्य धागे को अवरुद्ध करने के बारे में बात कर रहे हैं, asyncऔर मुख्य धागे को अवरुद्ध withContextनहीं करेंगे, तो वे केवल कोरटाइन के शरीर को निलंबित कर देंगे, जबकि कुछ लंबे समय तक चलने वाला कार्य चल रहा है और परिणाम की प्रतीक्षा कर रहा है। अधिक जानकारी और उदाहरण के लिए इस लेख को माध्यम पर देखें: कोटलिन कोरआउट्स के साथ Async ऑपरेशंस
योगेश उमेश वैट
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.