مقالات باگتو

Coroutine در Android (قسمت دوم): شروع کار
Coroutine در Android (قسمت دوم): شروع کار

این بخشی از یک  مقالهچند بخشی در مورد استفاده ازCoroutineها در اندروید است. این مقاله در مورد شروع کار و پیگیری کارهایی که آغاز شده است تمرکز دارد.

برای مطالعه قسمت اول  اینجاکلیک کنید.

برای مطالعه قسمت سوم اینجاکلیک کنید.

پیگیریCoroutine  ها

در قسمت اول، مشكلاتي را كهCoroutine  ها در حل آن بسيار خوب هستند، مورد بررسي قرار داديم. به عنوان یکrecap،Coroutine  ها یک راه حل عالی برای دو مشکل برنامه نویسی رایج هستند: 

  1. Long running tasksکارهایی هستند که خیلی طول می کشد تاmain threadمسدود شود.
  2. Main-safetyبه شما امکان می دهد اطمینان حاصل کنید که هرگونهمتد suspendرا می توان ازmain threadفراخوانی کرد.

برای حل این مشکلات،Coroutine  ها با اضافه کردن حالتsuspendوresume، بر روی متد هایregularبنا می شوند. هنگامی که تمامCoroutine  ها روی یک  threadخاص به حالت  suspendدرآیند ،threadبرای انجام سایر کارها آزاد است.

با این حال ،Coroutine  هابه خودی خود به شما کمک نمی کنند تا کارهایی که انجام می شود را پیگیری کنید. خیلی خوب است که تعداد زیادیCoroutine  داشته باشید و همه آنها را به طور همزمان  suspendکنید. و در حالی کهCoroutine  ها کم ارزش هستند ، کارهایی که انجام می دهند اغلب باارزش هستند ، مانند خواندن فایل ها یا درخواست های شبکه.

پیگیری صدهاCoroutine  به صورت دستی با استفاده از کد بسیار دشوار است. می توانید همه آنها را ردیابی کرده و به صورت دستی اطمینان حاصل کنید که آنها را کامل یا لغو کنید ، اما کدی مانند این خسته کننده و مستعد خطا است. اگر کد کامل نباشد ، یکCoroutine  را از دست می دهد ، همان چیزی است که من آن  work leakمی نامم.

work leakمانندmemory leakاست اما بدتر. این یک Coroutineگم شده است. علاوه بر استفاده از حافظه ، یکwork leakمی تواند برای استفاده ازCPU، دیسک یا حتی راه اندازی یک درخواست شبکه ، خود را از سر بگیرد.

یک  Coroutine,leakeشده می تواند حافظه ،CPU، دیسک را هدر دهد یا حتی درخواست شبکه ای را که لازم نیست راه اندازی کند

برای کمک به جلوگیری ازleaking coroutine، کاتلینstructured concurrencyرا معرفی کرد.Structured concurrencyاز ویژگی های زبان و بهترین شیوه هایی است که در صورت رعایت آنها به شما کمک می کند تا کلیه کارها را در  Coroutineنگه دارید.

درAndroid، ما می توانیم از  structured concurrencyبرای انجام سه کار استفاده کنیم:

  1. Cancel work وقتی دیگر به آن نیازی نیست.
  2. Keep track ردیابی کار وقتی در اجرا است.
  3. Signal errorsوقتی یکCoroutineنمیتواند کار را انجام دهد.

بیایید به هر یک از این موارد بررسی کنیم و ببینیم که چگونه  structured concurrencyبه ما کمک می کند تا مطمئن شویم که هرگز کارهای مربوط بهleak workرا از دست نمی دهیم.

کار را باscopeلغو کنید

در کاتلین،Coroutineها باید در چیزی به نامCoroutineScopeاجرا شوند.CoroutineScope,  Coroutineهای شما حتیCoroutineهایی کهsuspendهستندرا پیگیری می کند. بر خلافDispatcher که در قسمت یک در مورد آنها صحبت کردیم در واقع پیکربندی های شما را اجرا نمی کند ، این فقط اطمینان می دهد که آنها را از دست نخواهید داد.

برای اطمینان از ردیابی همهCoroutineها ، کاتلین به شما اجازه نمی دهد تا یکCoroutineScopeرا شروع کنید. شما می توانید از یکCoroutineScopeبه عنوان نسخه ای سبُک از یکExcerServiceبا ابرقدرت ها بیاندیشید. این امکان را به شما می دهد تا بتوانیدCoroutineهای جدیدی را شروع کنید به همراه همه آنهایی که متوقف شده اند از سر بگیرید.

یکCoroutineScopeتمامCoroutineهای شما را ردیابی می کند ، و می تواند تمامCoroutineهای شروع شده در آن را لغو کند. این به خوبی با توسعهAndroidمطابقت دارد و این اطمینان را به شما میدهد که وقتی کاربر صفحه را بست ، همه چیزهایی را که توسط یک صفحه شروع شده است را پاک میکند.

یکCoroutineScopeتمامCoroutineهای شما را ردیابی می کند ، و می تواند تمامCoroutineهای شروع شده در آن را لغو کند.

شروعCoroutineهای جدید

توجه به این نکته حائز اهمیت است که شما نمی توانید از هرجایی متد هایsuspendرا صدا کنید. مکانیسمsuspendوresume  مستلزم آن است که شما از متد های عادی به یکCoroutineسوئیچ کنید.

دو راه برای شروعCoroutineها وجود دارد و آنها کاربردهای متفاوتی دارند:

  1. launch builderیکCoroutineجدید را شروع می کند که " fire and forget" است - این بدان معناست که نتیجه را به تماس گیرنده بر نمی گرداند.
  2.  async builderیکCoroutineجدید شروع می کند ، و به شما امکان می دهد نتیجه ای را بامتدsuspendبه نام awaitمنتقل کنید.

تقریباً در همه موارد ، پاسخ صحیح برای چگونگی شروعCoroutineاز یکمتدregular استفاده ازlaunchاست.

از آنجا که  متدregularراهی برای فرا خوانی awaitندارد (بخاطر بسپارید ، نمی توان توابع  suspendرا مستقیماً فراخوانی کرد) منطقی نیست که ازasyncبه عنوان ورودی اصلی بهCoroutineاستفاده کنید.

در عوض باید با فراخوانی launch از coroutine scope استفاده کنید تا یک Coroutine را شروع کنید.

scope.launch {
    // This block starts a new coroutine 
    // "in" the scope.
    // 
    // It can call suspend functions
    fetchDocs()
}

 

شما می توانیدlaunchرا به عنوان پلی تصور کنید که کد شما را ازregularمتد هابه دنیایCoroutineمی برد. در داخل بدنهlaunchمی توانید متد هایsuspendرا فراخوانی کرده وmain safetyرا ایجاد کنید.

Launchپلی است از متد هایregular بهCoroutineها.

توجه : تفاوت زیادی بینlaunch وasyncنحوه مدیریتexceptionها است.asyncانتظار دارد که شما در نهایت برای دریافت نتیجه (یاexception) با متد فراخوان ارتباط برقرار کند بنابراین به طور پیش فرضexceptionرا ایجاد نمی کند. این بدان معناست که اگر ازasyncبرای راه اندازی یک  Coroutineجدید استفاده کنید ، به طور سایلنتexceptionها را حذف می کند.

از آنجا کهlaunchوasyncفقط درCoroutineScopeدر دسترس هستند هر  Coroutineکه ایجاد کنید همیشه با یکScopردیابی می شود. کاتلین به شما اجازه نمی دهد یک  Coroutineبدون ردیابی ایجاد کنید چون که برای جلوگیری ازwork leaksبسیار طول می کشد.

 

ازViewModelشروع کنید

بنابراین اگر یکCoroutineScopeتمامCoroutineهایی را که در آن  launchمی شوند ردیابی کند وlaunchیکCoroutineجدید ایجاد کند دقیقاً از کجا باید باlaunchتماس بگیرید وscopeهای خود را قرار دهید؟ و چه زمانی منطقی است که تمامCoroutineهایی که از یکscopeشروع شده اند لغو شوند؟

در اندروید ، اغلب منطبق است که یکCoroutineScopeرا با صفحه کاربر مرتبط کنید. با این کار می توانید ازleakکردنCoroutineیا انجام کارهای اضافی برای  ActivityیاFragmentهایی که دیگر مربوط به کاربر نیستند ، خودداری کنید. وقتی کاربر صفحه را میبندد CoroutineScopeمرتبط با صفحه می تواند همه کارها را لغو کند.

Structured concurrencyهنگام لغو یکscope  همه  Coroutineهای آن را لغو می کند.

هنگام ادغام  Coroutineها با اجزای معماریAndroid، معمولاً می خواهیدCoroutineها را درViewModelراه اندازی کنید. این یک مکان طبیعی است از آنجا که جدی ترین کارها از آنجا شروع می شود  و دیگر نگران نخواهید بود که با چرخش صفحه تمامCoroutineها را از بین ببرید.

برای استفاده از کوروتین درViewModel  می توانید از پسوندviewModelScopeاستفاده کنید ویژگی پسوند ازlifecycle-viewmodel-ktx:2.1.0-alpha04, viewModelScopeدر مسیر انتشار Lifecycle(v2.1.0) AndroidXاست و در حال حاضر به صورت آلفا است. از آنجا که کتابخانه در حال حاضر به زبان آلفا است ، ممکن است اشکالاتی وجود داشته باشد وAPIها ممکن است قبل از انتشار نهایی تغییر کنند.

نگاهی به این مثال بیندازید:

class MyViewModel(): ViewModel() {
    fun userNeedsDocs() {
        // Start a new coroutine in a ViewModel
        viewModelScope.launch {
            fetchDocs()
        }
    }
}

 

viewModelScopeبه طور خودکار هرگونه  Coroutineرا که با پاک شدن اینViewModel(هنگامی که متد  onCleared()فراخوانی میشود) لغو می کند. این معمولاً رفتار درستی است - اگر اسناد را نگرفته ایم و کاربر برنامه را بسته است ، احتمالاً در حال تکمیل درخواست باتری او را هدر می دهیم. و برای ایمنی بیشتر ، یکCoroutineScopeخود را گسترش می دهد. بنابراین ، اگر یکCoroutineشروع کنید و یکCoroutineدیگر را شروع کنید ، هر دو در یکScope قرار می گیرند. این بدان معناست که حتی زمانی که کتابخانه هایی که به آنها وابسته هستید ، ازviewModelScopeخود یک  Coroutineرا شروع می کنند راهی برای لغو آنها خواهید داشت!

توجه: با پرتابCancellationExceptionدر زمانsuspend،Coroutineلغو می شود. کنترل کننده هایExceptionکه دارای یکExceptionدر سطح بالا مانندThrowableهستند ، اینExceptionرا می گیرند. اگرExceptionرا در یک کنترل کنندهExceptionمصرف کنید ، یا هرگزsuspendنکنید ،Coroutineدر حالت نیمه لغو باقی می ماند.

بنابراین ، هنگامی که برای اجرای طولانی مدت مانند یکViewModelبه یکCoroutineنیاز دارید ، برای تغییر از توابعregularبهCoroutineها ، ازviewModelScopeاستفاده کنید. سپس ، از آنجا کهviewModelScopeبه طور خودکارCoroutineها را برای شما لغو می کند ، نوشتن یک حلقه بی نهایت در اینجا بدون ایجادleak کاملاً خوب است.

fun runForever() {
    // start a new coroutine in the ViewModel
    viewModelScope.launch {
        // cancelled when the ViewModel is cleared
        while(true) {
            delay(1_000)
            // do something every second
        }
    }
}

 

با استفاده ازviewModelScopeمی توانید اطمینان حاصل کنید که همه کارها ، حتی این حلقه نامحدود ، درصورت لغو کار دیگر لغو می شوند.

کار را پیگیری کنید

راه اندازی یکCoroutineخوب است  و برای تعداد زیادی کد که واقعاً تمام کاری است که شما باید انجام دهید. یکCoroutineرا راه اندازی کنید ، یک درخواست شبکه کنید و نتیجه را در پایگاه داده ذخیره کنید.

هر چند گاهی اوقات به کمی پیچیدگی بیشتری نیاز دارید. بگویید شما می خواستید دو درخواست شبکه را همزمان (یا در آن زمان) در یکCoroutineانجام دهید برای این کار باید تعداد بیشتریCoroutineرا شروع کنید!

برای ساختنCoroutineبیشتر ، هر متد متوقف شده می تواند با استفاده از سازنده(builder) دیگری به نامcoroutineScopeیاsupervisorScope،Coroutineبیشتری را شروع کند. CoroutineScopeسازنده(builder)وCoroutineScopeچیزهای مختلفی هستند با وجود اینکه فقط یک تفاوت در نام آنها وجود دارد.

راه اندازیCoroutineهای جدید در همه جا یکی از راه های ایجاد  work leaks احتمالی کار است. تماس گیرنده ممکن است ازCoroutineهای جدید اطلاع نداشته باشد و اگر اینگونه نیست ، چگونه می تواند کار را پیگیری کند؟

برای رفع این مشکل ،  structured concurrency(همزمانی ساختاریافته) به ما کمک می کند. یعنی ، این تضمین را می دهد که وقتی تابع  suspendبرمی گردد ، تمام کارهای آن انجام می شود

 structured concurrencyتضمین می کند که وقتی متد متوقف می شود ، همه کارهای آن انجام می شود.

در اینجا مثالی از استفاده ازcoroutineScopeبرای واکشی دو سند آورده شده است:

suspend fun fetchTwoDocs() {
    coroutineScope {
        launch { fetchDoc(1) }
        async { fetchDoc(2) }
    }
}

 

در این مثال ، دو سند به طور همزمان از شبکه واکشی می شود. اولین مورد در یکCoroutineآغاز شده است که " fire and forget" است این بدان معناست که نتیجه را به تماس گیرنده بر نمی گرداند. سند دوم باasyncواکشی می شود ، بنابراین سند می تواند به تماس گیرنده برگردانده شود. این مثال کمی عجیب است ، زیرا معمولاً ازasyncبرای هر دو سند استفاده می کنید - اما من می خواستم نشان دهم که بسته به آنچه نیاز دارید می توانیدlaunchوasyncرا با هم استفاده کنید.

coroutineScopeوsupervisorScopeبه شما امکان می دهند با خیال راحتCoroutineها را از متدهایsuspendراه اندازی کنید.

البته توجه داشته باشید که این کد هرگز صریحاً منتظر هیچ یک ازCoroutineهای جدید نیست! به نظر می رسد کهfetchTwoDocsبازگردد در حالی کهCoroutineها اجرا می شوند!

برای ایجادstructured concurrencyو جلوگیری از  work leaksمی خواهیم اطمینان حاصل کنیم که وقتی متدsuspendمانندfetchTwoDocsبرمی گردد ، همه کارهای آن انجام می شود. این بدان معناست که هر دو نوعCoroutineکه راه اندازی می شود باید قبل از بازگشتfetchTwoDocsکامل شوند.

کاتلین اطمینان حاصل می کند که کار از طریقfetchTwoDocsبا سازندهcoroutineScope,leakنمی کند. coroutineScopebuilderخود را به حالت  suspendدر می آورد تا زمانی که تمام  coroutineهای شروع شده در داخل آن کامل شود. به همین دلیل ، هیچ راهی برای بازگشت ازfetchTwoDocsوجود ندارد تا زمانی که تمام  Coroutineهای شروع شده در سازندهcoroutineScopeکامل شود.

 

کار زیاد و زیاد

اکنون که ردیابی یکی دوCoroutineرا بررسی کرده ایم ، وقت آن فرا رسیده است که سعی کنیم هزارCoroutineرا ردیابی کنیم!

به انیمیشن زیر نگاهی بیندازید:

 

انیمیشن نشان می دهد که چگونه یک coroutineScope می تواند هزار Coroutine را ردیابی کند.

این مثال ایجاد یک هزار درخواست شبکه به طور همزمان است. این در کد واقعیAndroidتوصیه نمی شود زیرا برنامه شما از منابع زیادی استفاده خواهد کرد.

در این کد ، ما یک هزارCoroutine را باlaunchدر داخل سازندهcoroutineScopeراه اندازی می کنیم. شما می توانید ببینید که چگونه همهsinceمی شود. از آنجا که ما در متدsuspendهستیم ، برخی از کدها در جایی باید ازCoroutineScopeبرای ایجاد یکCoroutine استفاده کرده باشند. ما در مورد آنCoroutineScopeچیزی نمی دانیم ، این می تواند یکviewModelScopeیاCoroutineScopeدیگری باشد که در جای دیگری تعریف شده باشد.آن Scopeکه فراخوانی شود مهم نیست ، سازندهcoroutineScopeاز آن به عنوان والدScopeجدیدی که ایجاد می کند استفاده خواهد کرد.

سپس در داخل بلوکcoroutineScope، launchشروع می شود درScope جدید. با شروع کاملCoroutine ها ،Scopeجدید ردیابی آنها را دنبال می کند. سرانجام ، به محض این که تمامCoroutine های شروع شده درCoroutineScopeکامل شدند ، بازگشتloadLotsآزاد است.

توجه: رابطه پدر و فرزند بینScopeها وCoroutine ها با استفاده از اشیاJob ایجاد می شود. اما شما اغلب می توانید بدون اینکه در آن سطح فرو بروید ، به رابطه بینCoroutine وScopeفکر کنید.

coroutineScopeوsupervisorScopeمنتظر کامل شدنCoroutine های فرزند خواهند بود.

با استفاده ازcoroutineScopeیاsupervisorScopeمی توانید یکCoroutine را با خیال راحت از هر  متدsuspendراه اندازی کنید. حتی اگر این کار یکCoroutine جدید را شروع کند ، شما به طور تصادفیleak workنخواهید کرد زیرا همیشه تماس گیرنده را تا زمان کامل شدنCoroutine جدید به حالتsuspendدر می آورید.

آنچه جالب استcoroutineScopeیکScopeبرای فرزند ایجاد می کند. بنابراین اگر Scopeپدر لغو شود ، این لغو را به همهCoroutine  های جدید منتقل می کند. اگر تماس گیرندهviewModelScopeبود ، وقتی کاربر صفحه را ببندد، همه یک هزارCoroutine به طور خودکار لغو می شدند. بسیار شسته و رفته!

قبل از اینکه به سراغ خطاها برویم ، ارزش دارد که لحظه ای  در موردsupervisorScopeدر مقابلcoroutineScopeصحبت کنیم. تفاوت اصلی در این است که هر زمان که فرزندانش از کار بیفتند ، یکcoroutineScopeلغو می شود. بنابراین ، اگر یک درخواست شبکه انجام نشود ، بقیه درخواست ها بلافاصله لغو می شوند. اگر در عوض می خواهید سایر درخواست ها را حتی در صورت عدم موفقیت ادامه دهید ، می توانید از یکsupervisorScopeاستفاده کنید. یکsupervisorScopeدر صورت عدم موفقیت یکی از فرزندان ، آنها را لغو نمی کند.

 

خطاهای سیگنال هنگام خراب شدن یکCoroutine 

درCoroutine ها ، خطاها با انداختنexception، درست مانند متد هایregular، علامت گذاری می شوند. exceptionها در  متدsuspendباresumeمجدداً به تماس گیرنده ارسال می شود. دقیقاً مانند متد هایregular، محدود به تلاش برای کنترل خطاها نیستید و در صورت تمایل می توانیدabstractionرا برای انجام مدیریت خطا با سبک های دیگر ایجاد کنید.

با این حال ، شرایطی وجود دارد که خطاها می توانند درCoroutine گم شوند.

val unrelatedScope = MainScope()
// example of a lost error
suspend fun lostError() {
    // async without structured concurrency
    unrelatedScope.async {
        throw InAsyncNoOneCanHearYou("except")
    }
}

 

توجه داشته باشید این کد در حال اعلامcoroutineScopeنامرتبط است که یکCoroutine جدید بدون structured concurrencyراه اندازی می کند. در ابتدا به یاد داشته باشید که من گفتم که structured concurrencyترکیبی از انواع و روشهای برنامه نویسی است و معرفیScopeهای مربوط بهCoroutine در توابعsuspendپیروی از روشهای برنامه نویسیstructured concurrencyنیست.

خطا در این کد از بین رفته است زیراasyncفرض می کند که شما در نهایت با فراخوانیawaitدر جایی که دوبارهexceptionرا ایجاد می کند تماس خواهید گرفت. با این حال ، اگر هرگز در انتظار فراخوانی نباشید ، اینexceptionبرای همیشه در انتظار برای فراخوانی ذخیره می شود.

Structured concurrencyتضمین می کند که وقتی خطای کوتاهی رخ می دهد ، تماس گیرنده یاscopeآن مطلع می شود.

اگر برای کد پایین از  Structured concurrencyاستفاده کنید ، خطا به درستی به تماس گیرنده منتقل می شود.

suspend fun foundError() {
    coroutineScope {
        async {
            throw StructuredConcurrencyWill("throw")
        }
    }
}

 

از آنجا کهcoroutineScopeمنتظر تکمیل همه فرزندان خواهد بود ، در صورت عدم موفقیت نیز می تواند به آنها اطلاع داده شود. اگر یکCoroutine شروع شده توسطcoroutineScopeیکexceptionرا ایجاد کند ،coroutineScopeمی تواند آن را به تماس گیرنده پرتاب کند.

 

استفاده از همزمانی ساختاری

در این مقاله، من همزمانی ساختاری را معرفی کردم و نشان دادم چگونه باعث می شود کد ما به خوبی باAndroid ViewModelمتناسب باشد تا از  work leaksجلوگیری کند.

 همچنین در مورد چگونگی استدلال در مورد متد هایsuspendراحت تر صحبت کردم. هم با اطمینان از اینکه آنها کار را قبل از بازگشت کامل انجام می دهند و هم با اطمینان از اینکه خطاها را با استفاده ازexception سیگنال می دهند.

اگر در عوض از  unstructured concurrencyاستفاده می کردیم ، برایCoroutine آسان است که به طور تصادفی  leak workرا که تماس گیرنده از آن نمی داند داشته باشد. کار قابل لغو نیست و تضمین نمی شود که ازexception  مجدداً اعاده می شوند. این باعث تعجب بیشتر کد ما می شود و احتمالاً اشکال مبهمی ایجاد می کند.

با معرفی یکCoroutineScopeغیر مرتبط یا با استفاده از یکScope,  globalبه نامGlobalScopeمی توانیدunstructured concurrencyایجاد کنید ، اما فقط باید فکر کنیدunstructured concurrencyدر موارد نادر بیشتر از فراخوانیScopeاست. ایده خوبی است که سپس خودتان ساختار را اضافه کنید تا اطمینان حاصل کنید که از unstructured coroutineیافته پیگیری نمی کنید ، خطاها را کنترل می کنید و داستان لغو خوبی دارید.

اگر تجربه  unstructured concurrencyرا دارید ،Structured concurrencyبه بعضی از موارد عادت می کند. ساختار و ضمانت ها تعامل با متدهایsuspendرا ایمن تر و آسان تر می کند. بهتر است تا آنجا که ممکن است باStructured concurrencyکار کنید ، زیرا باعث سهولت در خواندن کد می شود و بسیار تعجب آور نیست. در ابتدای این مقاله من سه مورد را ذکر کردم که همزمانی ساختاری برای ما حل می کند

  1. Cancel workوقتی دیگر نیازی به کار نیست ، آن را لغو کنید.

  2. Keep track    در حین کار کار را پیگیری کنید.

  3. Signal errors خطاهای سیگنال در صورت خراب شدن یک Coroutine.

برای دستیابی به اینstructured concurrency، برخی ضمانت ها در مورد کد ما ارائه می شود. در اینجا تضمین هایstructured concurrencyآورده شده است.

  1. وقتی یک  Scope لغو می شود ، همه نسخه های اصلی آن لغو می شوند.
  2. هنگامی که یک تابع تعلیق برمی گردد ، تمام کارهای آن انجام می شود.
  3. وقتی خطای کوتاهی رخ می دهد ، تماس گیرنده یا Scope آن مطلع می شود.

در کنار هم ، ضمانت های structured concurrencyباعث می شود که کد ما ایمن تر باشد ، استدلال در آن آسان تر باشد و به ما اجازه دهد ازleaking work جلوگیری کنیم!

 

 

در قسمت بعد چی داریم؟

در این مقاله نحوه راه اندازیCoroutine درAndroidدرViewModelو نحوه کار با  structured concurrencyرا بررسی کردیم تا کد ما کمتر تعجب آور باشد.

در پست بعدی ، ما بیشتر در مورد چگونگی استفاده ازCoroutine در شرایط عملی صحبت خواهیم کرد!

 

تگ‌ها
اشتراک

0 نظرات

    ;