Kotlin कोरूटीन की मदद से ऐप्लिकेशन की परफ़ॉर्मेंस को बेहतर बनाएं

Kotlin कोरूटीन की मदद से, एसिंक्रोनस कोड को साफ़ और आसान तरीके से लिखा जा सकता है. इससे लंबे समय तक चलने वाले टास्क मैनेज करने के दौरान भी, आपका ऐप्लिकेशन रिस्पॉन्सिव बना रहता है. जैसे, नेटवर्क कॉल या डिस्क ऑपरेशन.

इस विषय में, Android पर कोरूटीन के बारे में विस्तार से बताया गया है. अगर आपको कोरूटीन के बारे में जानकारी नहीं है, तो इस विषय को पढ़ने से पहले, Android पर Kotlin कोरूटीन के बारे में ज़रूर पढ़ें.

लंबे समय तक चलने वाले टास्क मैनेज करना

कोरूटीन, सामान्य फ़ंक्शन पर आधारित होते हैं. इनमें लंबे समय तक चलने वाले टास्क को मैनेज करने के लिए, दो कार्रवाइयां जोड़ी जाती हैं. invoke (या call) और return के अलावा, कोरूटीन में suspend और resume भी जोड़े जाते हैं:

  • suspend की मदद से, मौजूदा कोरूटीन के एक्ज़ीक्यूशन को रोका जा सकता है. साथ ही, सभी लोकल वैरिएबल सेव किए जा सकते हैं.
  • resume की मदद से, सस्पेंड किए गए कोरूटीन का एक्ज़ीक्यूशन, उस जगह से जारी रखा जा सकता है जहां उसे सस्पेंड किया गया था.

suspend फ़ंक्शन को सिर्फ़ अन्य suspend फ़ंक्शन से कॉल किया जा सकता है. इसके अलावा, नया कोरूटीन शुरू करने के लिए, launch जैसे कोरूटीन बिल्डर का इस्तेमाल किया जा सकता है.

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

suspend fun fetchDocs() { // Dispatchers.Main
    val result = get("https://developer.android.com") // Dispatchers.IO for `get`
    show(result) // Dispatchers.Main
}

suspend fun get(url: String) = withContext(Dispatchers.IO) {
}

इस उदाहरण में, get() अब भी मुख्य थ्रेड पर चलता है. हालांकि, यह नेटवर्क अनुरोध शुरू करने से पहले, कोरूटीन को सस्पेंड कर देता है. नेटवर्क अनुरोध पूरा होने पर, get मुख्य थ्रेड को सूचना देने के लिए कॉलबैक का इस्तेमाल करने के बजाय, सस्पेंड किए गए कोरूटीन को फिर से शुरू करता है.

Kotlin, यह मैनेज करने के लिए स्टैक फ़्रेम का इस्तेमाल करता है कि कौनसा फ़ंक्शन, किसी भी लोकल वैरिएबल के साथ चल रहा है. कोरूटीन को सस्पेंड करते समय, मौजूदा स्टैक फ़्रेम को कॉपी करके सेव कर लिया जाता है, ताकि इसे बाद में इस्तेमाल किया जा सके. फिर से शुरू करते समय, स्टैक फ़्रेम को उस जगह से कॉपी किया जाता है जहां उसे सेव किया गया था. इसके बाद, फ़ंक्शन फिर से चलने लगता है. भले ही, कोड सामान्य क्रम में चलने वाले ब्लॉकिंग अनुरोध की तरह दिखे, लेकिन कोरूटीन यह पक्का करता है कि नेटवर्क अनुरोध, मुख्य थ्रेड को ब्लॉक न करे.

मुख्य तौर पर सुरक्षा के लिए कोरूटीन इस्तेमाल करना

Kotlin कोरूटीन, यह तय करने के लिए डिस्पैचर का इस्तेमाल करते हैं कि कोरूटीन के एक्ज़ीक्यूशन के लिए किन थ्रेड का इस्तेमाल किया जाए. मुख्य थ्रेड के बाहर कोड चलाने के लिए, Kotlin कोरूटीन को डिफ़ॉल्ट या IO डिस्पैचर पर काम करने के लिए कहा जा सकता है. Kotlin में, सभी कोरूटीन को डिस्पैचर में चलाना ज़रूरी है. भले ही, वे मुख्य थ्रेड पर चल रहे हों. कोरूटीन खुद को सस्पेंड कर सकते हैं. साथ ही, उन्हें फिर से शुरू करने की ज़िम्मेदारी डिस्पैचर की होती है.

यह तय करने के लिए कि कोरूटीन कहां चलने चाहिए, Kotlin तीन डिस्पैचर उपलब्ध कराता है. इनका इस्तेमाल किया जा सकता है:

  • Dispatchers.Main - Android के मुख्य थ्रेड पर कोरूटीन चलाने के लिए, इस डिस्पैचर का इस्तेमाल करें. इसका इस्तेमाल सिर्फ़ यूज़र इंटरफ़ेस (यूआई) के साथ इंटरैक्ट करने और तेज़ी से काम करने के लिए किया जाना चाहिए. उदाहरण के लिए, suspend फ़ंक्शन को कॉल करना, Android यूआई फ़्रेमवर्क की कार्रवाइयां चलाना, और LiveData ऑब्जेक्ट अपडेट करना.
  • Dispatchers.IO - इस डिस्पैचर को मुख्य थ्रेड के बाहर, डिस्क या नेटवर्क I/O करने के लिए ऑप्टिमाइज़ किया गया है. उदाहरण के लिए, Room कॉम्पोनेंट का इस्तेमाल करना, फ़ाइलें पढ़ना या उनमें लिखना, और नेटवर्क से जुड़ी कोई भी कार्रवाई करना.
  • Dispatchers.Default - इस डिस्पैचर को मुख्य थ्रेड के बाहर, सीपीयू-इंटेंसिव काम करने के लिए ऑप्टिमाइज़ किया गया है. इस्तेमाल के उदाहरणों में, सूची को क्रम से लगाना और JSON को पार्स करना शामिल है.

पिछले उदाहरण को जारी रखते हुए, get फ़ंक्शन को फिर से तय करने के लिए, डिस्पैचर का इस्तेमाल किया जा सकता है. get के मुख्य हिस्से में, IO थ्रेड पूल पर चलने वाला ब्लॉक बनाने के लिए, withContext(Dispatchers.IO) को कॉल करें. उस ब्लॉक में मौजूद कोई भी कोड, हमेशा IO डिस्पैचर के ज़रिए एक्ज़ीक्यूट होता है. withContext खुद एक सस्पेंड फ़ंक्शन है. इसलिए, get फ़ंक्शन भी एक सस्पेंड फ़ंक्शन है.

suspend fun fetchDocs() { // Dispatchers.Main
    val result = get("developer.android.com") // Dispatchers.Main
    show(result) // Dispatchers.Main
}

suspend fun get(url: String) = // Dispatchers.Main
    withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block)
        /* perform network IO here */ // Dispatchers.IO (main-safety block)
    } // Dispatchers.Main

कोरूटीन की मदद से, थ्रेड को सटीक तरीके से कंट्रोल किया जा सकता है. withContext() की मदद से, कॉलबैक का इस्तेमाल किए बिना, कोड की किसी भी लाइन के थ्रेड पूल को कंट्रोल किया जा सकता है. इसलिए, इसे डेटाबेस से पढ़ने या नेटवर्क अनुरोध करने जैसे बहुत छोटे फ़ंक्शन पर लागू किया जा सकता है. withContext() का इस्तेमाल करना एक अच्छी प्रैक्टिस है, ताकि यह पक्का किया जा सके कि हर फ़ंक्शन मुख्य तौर पर सुरक्षित हो. इसका मतलब है कि आप मुख्य थ्रेड से फ़ंक्शन को कॉल कर सकते हैं. इस तरह, कॉलर को यह सोचने की ज़रूरत नहीं होती कि फ़ंक्शन को एक्ज़ीक्यूट करने के लिए कौनसा थ्रेड इस्तेमाल किया जाना चाहिए.

पिछले उदाहरण में, fetchDocs() मुख्य थ्रेड पर एक्ज़ीक्यूट होता है. हालांकि, यह सुरक्षित तरीके से get को कॉल कर सकता है, जो बैकग्राउंड में नेटवर्क अनुरोध करता है. कोरूटीन, suspend और resume के साथ काम करते हैं. इसलिए, withContext ब्लॉक पूरा होने के बाद, मुख्य थ्रेड पर मौजूद कोरूटीन को get के नतीजे के साथ फिर से शुरू किया जाता है.

withContext() की परफ़ॉर्मेंस

withContext() से, कॉलबैक पर आधारित किसी भी लागू करने के तरीके की तुलना में, ज़्यादा ओवरहेड नहीं जुड़ता. इसके अलावा, कुछ मामलों में कॉलबैक पर आधारित किसी भी लागू करने के तरीके की तुलना में, withContext() कॉल को ऑप्टिमाइज़ किया जा सकता है. उदाहरण के लिए, अगर कोई फ़ंक्शन नेटवर्क पर दस कॉल करता है, तो Kotlin को सिर्फ़ एक बार थ्रेड स्विच करने के लिए कहा जा सकता है. इसके लिए, बाहरी withContext() का इस्तेमाल करें. इसके बाद, भले ही नेटवर्क लाइब्रेरी withContext() का इस्तेमाल कई बार करे, लेकिन यह उसी डिस्पैचर पर बनी रहती है और थ्रेड स्विच नहीं करती. इसके अलावा, Kotlin, Dispatchers.Default और Dispatchers.IO के बीच स्विच करने की प्रोसेस को ऑप्टिमाइज़ करता है, ताकि जहां तक हो सके, थ्रेड स्विच न किए जाएं.

कोरूटीन शुरू करना

कोरूटीन को दो तरीकों से शुरू किया जा सकता है:

  • launch एक नया कोरूटीन शुरू करता है और कॉलर को नतीजा नहीं दिखाता. किसी भी ऐसे काम को शुरू करने के लिए launch का इस्तेमाल किया जा सकता है जिसे "फ़ायर ऐंड फ़ॉरगेट" माना जाता है.
  • async एक नया कोरूटीन शुरू करता है. साथ ही, आपको await नाम के सस्पेंड फ़ंक्शन की मदद से, नतीजा दिखाने की अनुमति देता है.

आम तौर पर, आपको किसी सामान्य फ़ंक्शन से नया कोरूटीन launch करना चाहिए, क्योंकि सामान्य फ़ंक्शन await को कॉल नहीं कर सकता. async का इस्तेमाल सिर्फ़ तब करें, जब किसी दूसरे कोरूटीन के अंदर या सस्पेंड फ़ंक्शन के अंदर, पैरलल डीकंपोज़िशन किया जा रहा हो.

पैरलल डीकंपोज़िशन

suspend फ़ंक्शन के अंदर शुरू किए गए सभी कोरूटीन को, उस फ़ंक्शन के रिटर्न होने पर रोकना ज़रूरी है. इसलिए, आपको यह पक्का करना होगा कि वे कोरूटीन, रिटर्न होने से पहले पूरे हो जाएं. Kotlin में स्ट्रक्चर्ड कॉनकरेंसी की मदद से, coroutineScope तय किया जा सकता है. यह एक या एक से ज़्यादा कोरूटीन शुरू करता है. इसके बाद, await() (एक कोरूटीन के लिए) या awaitAll() (एक से ज़्यादा कोरूटीन के लिए) का इस्तेमाल करके, यह पक्का किया जा सकता है कि ये कोरूटीन, फ़ंक्शन से रिटर्न होने से पहले पूरे हो जाएं.

उदाहरण के लिए, आइए एक coroutineScope तय करें, जो एसिंक्रोनस तरीके से दो दस्तावेज़ फ़ेच करता है. हर डिफ़र्ड रेफ़रंस पर await() को कॉल करके, हम यह पक्का करते हैं कि async की दोनों कार्रवाइयां, वैल्यू रिटर्न करने से पहले पूरी हो जाएं:

suspend fun fetchTwoDocs() =
    coroutineScope {
        val deferredOne = async { fetchDoc(1) }
        val deferredTwo = async { fetchDoc(2) }
        deferredOne.await()
        deferredTwo.await()
    }

कलेक्शन पर awaitAll() का इस्तेमाल भी किया जा सकता है. जैसा कि यहां दिए गए उदाहरण में दिखाया गया है:

suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main)
    coroutineScope {
        val deferreds = listOf( // fetch two docs at the same time
            async { fetchDoc(1) }, // async returns a result for the first doc
            async { fetchDoc(2) } // async returns a result for the second doc
        )
        deferreds.awaitAll() // use awaitAll to wait for both network requests
    }

भले ही, fetchTwoDocs() async की मदद से नए कोरूटीन लॉन्च करता हो, लेकिन फ़ंक्शन, लॉन्च किए गए उन कोरूटीन के पूरे होने का इंतज़ार करने के लिए awaitAll() का इस्तेमाल करता है, ताकि वह रिटर्न हो सके. हालांकि, ध्यान दें कि अगर हमने awaitAll() को कॉल नहीं किया होता, तब भी coroutineScope बिल्डर, fetchTwoDocs को कॉल करने वाले कोरूटीन को तब तक फिर से शुरू नहीं करता, जब तक सभी नए कोरूटीन पूरे नहीं हो जाते.

इसके अलावा, coroutineScope, कोरूटीन से थ्रो किए गए किसी भी अपवाद को पकड़ता है और उन्हें कॉलर को वापस भेजता है.

पैरलल डीकंपोज़िशन के बारे में ज़्यादा जानने के लिए, सस्पेंडिंग फ़ंक्शन कंपोज़ करना लेख पढ़ें.

कोरूटीन के सिद्धांत

CoroutineScope

A CoroutineScope का इस्तेमाल करके बनाए गए किसी भी कोरूटीन को ट्रैक करता है.launchasync चल रहे काम (यानी, चल रहे कोरूटीन) को किसी भी समय scope.cancel() को कॉल करके रद्द किया जा सकता है. Android में, कुछ KTX लाइब्रेरी, लाइफ़साइकल की कुछ क्लास के लिए अपना CoroutineScope उपलब्ध कराती हैं. उदाहरण के लिए, ViewModel में एक viewModelScope होता है, और Lifecycle में lifecycleScope होता है. हालांकि, डिस्पैचर के उलट, CoroutineScope, कोरूटीन नहीं चलाता.

viewModelScope का इस्तेमाल, Android पर कोरूटीन की मदद से बैकग्राउंड थ्रेडिंग में दिए गए उदाहरणों में भी किया जाता है. हालांकि, अगर आपको अपने ऐप्लिकेशन की किसी खास लेयर में कोरूटीन के लाइफ़साइकल को कंट्रोल करने के लिए, अपना CoroutineScope बनाना है, तो इसे इस तरह बनाया जा सकता है:

class ExampleClass {

    // Job and Dispatcher are combined into a CoroutineContext which
    // will be discussed shortly
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine within the scope
        scope.launch {
            // New coroutine that can call suspend functions
            fetchDocs()
        }
    }

    fun cleanUp() {
        // Cancel the scope to cancel ongoing coroutines work
        scope.cancel()
    }
}

रद्द किया गया स्कोप, ज़्यादा कोरूटीन नहीं बना सकता. इसलिए, scope.cancel() को सिर्फ़ तब कॉल करना चाहिए, जब उसके लाइफ़साइकल को कंट्रोल करने वाली क्लास डिस्ट्रॉय हो रही हो. viewModelScope का इस्तेमाल करते समय, the ViewModel क्लास, ViewModel's onCleared() तरीके में आपके लिए the scope को अपने-आप रद्द कर देती है.

नौकरी

A Job कोरूटीन का हैंडल होता है. launch या async की मदद से बनाया गया हर कोरूटीन, Job का एक इंस्टेंस रिटर्न करता है. यह इंस्टेंस, कोरूटीन की यूनीक तरीके से पहचान करता है और उसके लाइफ़साइकल को मैनेज करता है. Job को CoroutineScope में पास करके, उसके लाइफ़साइकल को और मैनेज किया जा सकता है. जैसा कि यहां दिए गए उदाहरण में दिखाया गया है:

class ExampleClass {
    // ...
    fun exampleMethod() {
        // Handle to the coroutine, you can control its lifecycle
        val job = scope.launch {
            // New coroutine
        }

        if (condition) {
            // Cancel the coroutine started above, this doesn't affect the scope
            // this coroutine was launched in
            job.cancel()
        }
    }
}

CoroutineContext

CoroutineContext , एलिमेंट के इस सेट का इस्तेमाल करके, कोरूटीन के व्यवहार को तय करता है:

  • Job: कोरूटीन के लाइफ़साइकल को कंट्रोल करता है.
  • CoroutineDispatcher: काम को सही थ्रेड पर भेजता है.
  • CoroutineName: कोरूटीन का नाम. यह डीबग करने के लिए काम का है.
  • CoroutineExceptionHandler: बिना हैंडल किए गए अपवादों को हैंडल करता है.

किसी स्कोप में बनाए गए नए कोरूटीन के लिए, नए कोरूटीन को Job का एक नया इंस्टेंस असाइन किया जाता है. साथ ही, CoroutineContext के अन्य एलिमेंट, शामिल करने वाले स्कोप से इनहेरिट किए जाते हैं. launch या async फ़ंक्शन में, CoroutineContext का नया इंस्टेंस पास करके, इनहेरिट किए गए एलिमेंट को बदला जा सकता है. ध्यान दें कि launch या async में Job पास करने से कोई फ़र्क़ नहीं पड़ता, क्योंकि Job का एक नया इंस्टेंस हमेशा नए कोरूटीन को असाइन किया जाता है.

class ExampleClass {
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine on Dispatchers.Main as it's the scope's default
        val job1 = scope.launch {
            // New coroutine with CoroutineName = "coroutine" (default)
        }

        // Starts a new coroutine on Dispatchers.Default
        val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) {
            // New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
        }
    }
}

कोरूटीन के अन्य संसाधन

कोरूटीन के अन्य संसाधन देखने के लिए, ये लिंक देखें: