یکی از ویژگیهایی که کاتلین رو از جاوا متمایز میکنه وجود کتابخونه Coroutines در اونه ، Coroutines چیه ؟ قبل از اون بگذارید یک سری مفاهیم رو با هم بررسی کنیم :
برنامه نویسی آسنکرون و سنکرون - Synchronous & Asynchronous
اگه دانشجوی رشته نرم افزار بوده باشید و درس هایی مثل مدارمنطقی ، معماری ، طراحی دیجیتال یا ... رو گذرونده باشید باید این دو کلمه رو بشناسید ، وقتی ما میگیم سنکرون منظورمون اینه که همه چیز به صورت یک توالی پشت سر هم اجرا میشه ، اگه ما میخوایم B رو اجرا کنیم باید قبلش A تموم شده باشه ، وقتی میگیم آسنکرون یعنی A و B میتونن هم زمان اجرا بشن . آیا این همون مفهوم Multi-threading هست ؟ جواب منفیه ! به شکل زیر نگاه کنید :
همون طوری که تو عکس میبینید ما سه Task رو به صورت سنکرون اجرا کردیم ، B وقتی اجرا میشه که حتما A قبلش اجرا شده باشه و C وقتی اجرا میشه که حتما B قبلش اجرا شده باشه ، برای آسنکرون هم میتونیم این شکل رو داشته باشیم :
همون طوری که اینجا میبینیم ما برای پیاده سازی آسنکرون در یک Thread اومدیم هر Task رو به قسمت های کوچک تر تقسیم کردیم و حالت موازی اجرا شدن رو در یک Thread پیاده کردیم ، برای muti-threading این قسمت هم که توضیح خاصی نیاز نداره
پس تا الان شما تفاوت آسنکرون و multi-threading رو باید فهمیده باشید (این مفهوم خیلی مهمه چون اکثر برنامه نویسا این دو رو با هم اشتباه میگیرن) ، حالا باید بریم ببینیم Coroutines چیه.
Coroutines
این کلمه از ترکیب دو کلمه Co و Routines درست شده که Co مخفف Coopreate که معنی همکاری رو میده و Routines هم به معنی کارکردها (توابع) هست ، طبق تعریف خودِ گوگل (البته مفهوم Coroutines به گوگل ، اندروید و یا کاتلین ربطی نداره و اصولا ما این مفهوم رو در خیلی از زبان های برنامه نویسی داریم) برای Coroutines :
A coroutine is a concurrency design pattern that you can use on Android to simplify code that executes asynchronously
کوروتین یک الگوی طراحی است که برای ساده سازی اجرای عملیاتهای آسنکرون استفاده میشود
همین ابتدای امر باید بگم که Coroutines لزوما به معنای اجرای عملیات در یک Thread دیگه نیست بلکه یکی از راه های اجرای عملیات در یک Thread دیگه Coroutines هست (ولی نه برعکس) ! مثلا وقتی که شما قصد دارید یک وب سرویس رو در اندروید اجرا کنید ، این یکی از عملیات هایی هست که میتونه با Coroutines پیاده سازی بشه . در Coroutines ما یک سری Job تعریف میکنیم و میتونیم همه اون ها رو در Thread اصلی (Main) اجرا کنیم یا در Background Thread (همین مفهوم رو در RxJava هم داشتیم ، میتونید به مقاله من در این مورد مراجعه کنید) . همون طوری که در برنامه نویسی عادی ما توابع رو تعریف میکنیم در Coroutines هم این توابع رو داریم ولی با یک شکل خاص :
private suspend fun doTask1()
کلمه کلیدی suspend مشخص میکنه این تابع برای Coroutines هست ، کاری که suspend میکنه اینه که موجب میشه Coroutines بتونه بعدا pause یا resume بشه ، شما میتونید کلمه suspend رو بعضی جاها نذارید ولی اگه این کارو بکنید باعث میشه نتونید درون اون تابع از ویژگی های Coroutines استفاده کنید (مثلا تابعی به اسم delay هست که توضیح خواهم داد ، اگه suspend رو نذارید نمیتونید درون اون تابع ازش استفاده کنید) .
این توابع که suspend رو اولشون میذاریم کجا اجرا میشن ؟ شما تو حالت عادی نمیتونید این تابع رو صدا بزنید ، شما یک تابع suspend رو یا باید از درون یک تابع suspend دیگه صدا بزنید یا اونو از طریق CoroutineScope صدا بزنید ، CoroutineScope یک interface هست که برای ما گروه بندی لازم برای اجرای عملیاتهای Coroutines های مختلف رو عملی میکنه ، CoroutineScope در سه حالت IO ، Main و Default میتونه استفاده بشه ، IO برای کارهایی مثل رکوئست وب و Main یعنی Thread اصلی ، مثلا به این نمونه دقت کنید :
CoroutineScope(IO).launch { doTask1() }
private suspend fun doTask() { Log.d("TASK1","My Task"); }
در اینجا کاری که میکنیم اینه : یک Coroutines جدید ایجاد شده و بر روی Background Thread برای ما یک تابع به نام doTask رو اجرا میکنه ، میتونیم کمی تاخیر برای این عملیات بذاریم :
private suspend fun doTask1() { delay(1000) Log.d("TASK1","My Task"); }
تابع delay چیه ؟ احتمالا با این دستور آشنایید :
Thread.sleep(1000)
باید بگم که دستور delay با دستور بالا کاملا فرق میکنه ، اگه شما sleep رو از کلاس Thread صدا بزنید کلِ Thread که درونش این تابع صدا زده شده از حرکت باز میمونه اما اگه delay رو صدا بزنید فقط همون Coroutines که درونش صدا زده شده به مدت مشخص درونش وقفه میفته (بالاتر گفتم که شما در یک Thread میتونید چندین Coroutines اجرا کنید ، Coroutines رو معادل کلمه Job بگیرید ، انجام یک کار)
خب ما در دستورات بالا در Background Thread عملیات رو اجرا کردیم ، اگه بخوایم رو UI چیزی رو تغییر بدیم دچار Crash میشیم ، بنابراین باید راهی رو پیدا کنیم که بتونیم مسیر کارهایی که انجام میدیم رو بین Thread های مخلتف تغییر بدیم ، یه راه اینه که یک Coroutines جدید اجرا کنیم و این بار روی Main Thread تنظمیش کنیم :
private suspend fun doTask1() { delay(1000) Log.d("TASK1","My Task"); CoroutineScope(Main).launch { // do some UI } }
و یک راه زیباتر به صورت زیره :
private suspend fun doTask1() { delay(1000) Log.d("TASK1","My Task"); withContext(Main){ // do some UI } }
عبارت withContext میتونه مسیر حرکت Coroutines رو بین Thread های مختلف تغییر بده و ورودیش هم دقیقا مثل CoroutineScope هست .
حالا job چیه ؟ کاری که ما کردیم در واقع launch کردن یه job بوده ، به عکس زیر اگه نگاه کنید متوجه میشید :
ما مجموعهای از job ها رو با دستورات بالا میتونستیم اجرا کنیم و براشون delay بذاریم ، تا الان باید متوجه شده باشید فرق Thread و Job و Thread.Sleep و delay چیه ، چند تا مثال دیگه هم ببینیم :
هدف اینه که یک کانتر بسازیم و در Background Thread برای ما شمارش کنه و در UI Thread یا Main Thread برای ما آپدیت کنه :
اول از همه job رو تعریف میکنیم :
lateinit var job : Job
بعد از اون دو دکمه خواهیم داشت ، یکی برای شروع کار و یکی برای لغو کار :
btn.setListener { if(!::job.isInitialized) createJob() } btn_cancel.setListener { if(::job.isInitialized) job.cancel() }
در اینجا یک مفهوم جدید داریم ، اون دو نقطه چیه ؟ وقتی شما یک متغیر داشته باشید و به صورت lateinit var تعریف کرده باشیدش میتونید با این دستور بفهمید مقداری دهی شده یا نه :
::varName.isInitialized
اپراتور دو نقطه البته جور های دیگه ای هم هست که برای رفرنس دادن به تابع استفاده میشه که از بحث ما خارجه و شما میتونید در اینجا اونو مطالعه کنید .
خب الان قضیه اینه که ما نگاه میکنیم اگه job ساخته نشده بود با دکمه btn اونو ایجاد میکنیم و اگه ساخته شده بود با دکمه btn_cancel اونو لغو میکنیم ، برای تابع createJob :
private fun createJob() { job = Job() job.invokeOnCompletion { it?.message.let { if(it != null) toast(it) } } counter(job) }
در این تابع ما میاییم و job رو مقداردهی میکنیم و ازش شی میگیریم ، تابع invokeOnCompletion میاد بررسی میکنه که job کامل شده یا لغو شده یا ... ، اگه لغو شده بود متغیر it که از جنس Throwable هست (چون به صورت لامبدا نوشته شده (البته بعضی ها هم میگن لمبدا و بعضیا لاندا و ... که طبق ویکیپدیا همون لامبدا درست تره) برای ما جنسش رو تو ویرگول ننوشته ، در اندروید استدیو اما مشخص میکنه) رو داریم و میتونیم پیغامی متناسب برای کاربر ایجاد کنیم ، دو تابع toast و counter رو نیاز داریم :
fun toast(str : String) { GlobalScope.launch(Main) { Toast.makeText(this@MainActivity, str,Toast.LENGTH_SHORT).show() } }
در تابع toast میاییم اسکوپ رو روی Main Thread میندازیم تا بتونیم UI رو آپدیت کنیم ، در تابع counter :
private fun counter(job : Job) { if(counter < maxCount) { CoroutineScope(IO + job).launch { counter++ toast(counter.toString()) delay(1000) createJob() } } }
میاییم بررسی میکنیم اگه counter از maxCount کوچکتر بود بیاد و برای ما در IO Thread یا همون Thread بک گراند عملیاتی رو انجام بده که در اینجا اون عملیات شمارش و اضافه کردن به counter هست ، delay رو هم برای اینکه بتونیم ریسپانس رو در UI راحت ببینیم گذاشتیم تا شبیه سازی شده باشه . در ورودی CoroutineScope علاوه بر مشخص کردن Thread اومدیم job رو هم پاس دادیم ، شما با این کار job رو به این Scope اختصاص میدید و یک Context جدید به این Scope اختصاص داده میشه که با بقیه متفوات خواهد بود ، مثلا اگه شما این کار رو نمیکردید هر جایی که مه این دستور رو داشتیم :
CoroutineScope(IO).launch{ ...}
و میومدیم delay انجام میدادیم ، این delay در کل این Scope ها عمل میکرد چون Context هاشون یکی بود ولی جوری که در کد بالاتر زدیم یک Context کاملا جدید تولید شده . counter و maxCounter رو هم در بالا تعریف میکنیم :
val maxCount = 10 var counter = 0
فرایند به این صورت دراومده که ما با کلیک بر روی btn بررسی میکنیم که job مقداردهی شده یا نه ، اگه نشده بود ایجادش میکنیم و مرتبا تا زمانی که counter ما به 10 برسه اونو اجرا میکنیم ، اگه در این بین btn_cancel رو بزنیم job ما لغو میشه و پیغام مناسب رو به کاربر نشون میده .
این مقاله شما رو به صورت مقدماتی به Coroutine آشنا کرد ، در مقالات بعدی چگونگی استفاده عملی در RestAPI و ... رو آموزش میدم ، لینک کد مقاله :