این بخشی از یک مقاله چند بخشی در مورد استفاده از Coroutine ها در اندروید است. این مقاله در مورد شروع کار و پیگیری کارهایی که آغاز شده است تمرکز دارد.
برای مطالعه قسمت اول اینجا کلیک کنید.
برای مطالعه قسمت سوم اینجا کلیک کنید.
پیگیری Coroutine ها
در قسمت اول ، مشكلاتي را كه Coroutine ها در حل آن بسيار خوب هستند، مورد بررسي قرار داديم. به عنوان یک recap ، Coroutine ها یک راه حل عالی برای دو مشکل برنامه نویسی رایج هستند:
- Long running tasks کارهایی هستند که خیلی طول می کشد تا main thread مسدود شود.
- 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 برای انجام سه کار استفاده کنیم:
- Cancel work وقتی دیگر به آن نیازی نیست.
- Keep track ردیابی کار وقتی در اجرا است.
- 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 ها وجود دارد و آنها کاربردهای متفاوتی دارند:
- launch builder یک Coroutine جدید را شروع می کند که " fire and forget" است - این بدان معناست که نتیجه را به تماس گیرنده بر نمی گرداند.
- 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 کار کنید ، زیرا باعث سهولت در خواندن کد می شود و بسیار تعجب آور نیست. در ابتدای این مقاله من سه مورد را ذکر کردم که همزمانی ساختاری برای ما حل می کند
Cancel work وقتی دیگر نیازی به کار نیست ، آن را لغو کنید.
Keep track در حین کار کار را پیگیری کنید.
Signal errors خطاهای سیگنال در صورت خراب شدن یک Coroutine .
برای دستیابی به این structured concurrency ، برخی ضمانت ها در مورد کد ما ارائه می شود. در اینجا تضمین های structured concurrency آورده شده است.
- وقتی یک Scope لغو می شود ، همه نسخه های اصلی آن لغو می شوند.
- هنگامی که یک تابع تعلیق برمی گردد ، تمام کارهای آن انجام می شود.
- وقتی خطای کوتاهی رخ می دهد ، تماس گیرنده یا Scope آن مطلع می شود.
در کنار هم ، ضمانت های structured concurrency باعث می شود که کد ما ایمن تر باشد ، استدلال در آن آسان تر باشد و به ما اجازه دهد از leaking work جلوگیری کنیم!
در قسمت بعد چی داریم؟
در این مقاله نحوه راه اندازی Coroutine در Android در ViewModel و نحوه کار با structured concurrency را بررسی کردیم تا کد ما کمتر تعجب آور باشد.
در پست بعدی ، ما بیشتر در مورد چگونگی استفاده از Coroutine در شرایط عملی صحبت خواهیم کرد!
برای افزودن دیدگاه خود، نیاز است ابتدا وارد حساب کاربریتان شوید