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

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

इस विषय में, 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 को बताएं कि वह Default या IO डिस्पैचर पर काम करे. Kotlin में, सभी कोरूटीन को डिस्पैचर में चलाना ज़रूरी है. भले ही, वे मुख्य थ्रेड पर चल रही हों. कोरूटीन खुद को निलंबित कर सकती हैं. साथ ही, उन्हें फिर से शुरू करने की ज़िम्मेदारी डिस्पैचर की होती है.

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

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

पिछले उदाहरण को जारी रखते हुए, get फ़ंक्शन को फिर से तय करने के लिए, डिस्पैचर का इस्तेमाल किया जा सकता है. get के मुख्य हिस्से में, withContext(Dispatchers.IO) को कॉल करके एक ऐसा ब्लॉक बनाएं जो 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() का इस्तेमाल करें, ताकि यह पक्का किया जा सके कि हर फ़ंक्शन main-safe है. इसका मतलब है कि मुख्य थ्रेड से फ़ंक्शन को कॉल किया जा सकता है. इस तरह, कॉल करने वाले को यह सोचने की ज़रूरत नहीं होती कि फ़ंक्शन को लागू करने के लिए किस थ्रेड का इस्तेमाल किया जाना चाहिए.

पिछले उदाहरण में, fetchDocs() को मुख्य थ्रेड पर लागू किया गया है. हालांकि, यह get को सुरक्षित तरीके से कॉल कर सकता है. 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

CoroutineScope, launch या async का इस्तेमाल करके बनाए गए किसी भी कोरूटीन को ट्रैक करता है. 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 का इस्तेमाल करने पर, ViewModel क्लास, ViewModel के onCleared() तरीके में स्कोप को अपने-आप रद्द कर देती है.

नौकरी

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 पास करें. ध्यान दें कि Job को launch या async में पास करने से कोई असर नहीं पड़ता, क्योंकि 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)
        }
    }
}

को-रूटीन से जुड़े अन्य संसाधन

को-रूटीन के बारे में ज़्यादा जानने के लिए, यहां दिए गए लिंक देखें: