Kotlin Coroutines چیست؟ آموزش صفر تا صد کاتلین کوروتین

  • حسین کرمی
  • 13 اسفند 1400
What is kotlin coroutines? Tutorial for coroutines

Kotlin coroutines یک راه بسیار عالی برای نوشتن کد به شکل غیر همزمان (asynchronous) است که کاملا خوانا و راحت است. کاتلین با یک ساختار بسیار ساده کاتلین کوروتین را ارائه می‌دهد: کلمه کلیدی suspend. کوروتین شبیه نخ‌ها (Thread) عمل می‌کنند اما در واقع ترد نیستند و تفاوت‌های بنیادینی با آن‌ها دارند. بهترین تعریف از coroutines جاب‌ها (Jobs) است.

در این پست قصد داریم اصول کوروتین‌ها و توابع suspend را به شما یاد دهیم. البته قبل از اینکه آموزش coroutines در کاتلین را به صورت کدی آغاز کنیم، بهتر است با مفهوم کروتینز آشنا شویم. با ما همراه باشید.

ممکن است به مطالب زیر علاقه‌مند باشید:

Kotlin coroutines چیست؟

کوروتین در کاتلین 1.1 معرفی شد. کاتلین یک روش جدید برای برنامه‌نویسی نا‌همزمان و غیر متوقف شونده (non-blocking) معرفی کرد. یک کد نا‌همزمان (asynchronous) کدی است که به صورت موازی اجرا می‌شود. همچنین به آن non-blocking نیز می‌گویند زیرا در نتیجه استفاده از آن باعث می‌شود که نخ اصلی اپلیکیشن (Main Thread) متوقف نشود. در حالت همزمان کد‌ها به صورت خط به خط اجرا می‌شوند.

فرض کنید برنامه شما به یک خط رسیده است که باید اطلاعاتی را از بستر اینترنت دریافت کند. سرعت اینترنت شما ممکن است پایین باشد و یا زمان زیادی نیاز شود که اپلیکیشن منتظر دریافت پاسخ از سرور شود. چه اتفاقی برای برنامه شما خواهد افتاد؟ برنامه شما قفل می‌شود و حتی ممکن است رابط کاربری (UI) که وظیفه تعامل با کاربر را دارد نیز قفل شود و یا در بهترین حالت لگی شود. راهکار این مشکل چیست؟ برای جلوگیری از عدم اتفاق چنین مشکلی، درخواست به سرور را به صورت موازی با برنامه اصلی اجرا می‌کنند. مزیت اصلی آن این است که از فرآیند اصلی برنامه شما جدا می‌شود و به صورت جداگانه کار خود را انجام می‌دهد. ما نیز هر زمان که به پاسخ رسید، می‌توانیم در فرآیند اصلی برنامه نتیجه را نمایش دهیم.

به تردی که وظیفه نمایش رابط کاربری (UI) را به کاربر دارد، Main Thread یا UI Thread گفته می‌شود و به سایر ترد‌هایی که به صورت موازی و در پس زمینه اجرا می‌شوند، Helper Thread گفته می‌شود.

 

چرا باید از کوروتینز در کاتلین استفاده کنیم؟

همانگونه که احتمالا شما نیز می‌دانید، در زبان جاوا و برنامه‌نویسی اندروید، روش‌های بسیاری برای برنامه‌نویسی موازی یا Async وجود دارد. می‌توانید از Thread، AsyncTask، Rxjava و... استفاده کنید. پس چرا باید از کوروتینز استفاده کنید؟

اگر از Rxjava استفاده کرده باشید، احتمالا می‌دانید که زمان زیادی را باید بگذارید تا بتوانید به تجربه کافی برای استفاده بدون مشکل و امن از آن برسید. AsyncTask نیز ابزاری است که فقط برای کار‌های کم حجم کاربرد دارد و نمی‌توان از آن برای کار‌های زمانبر و نسبتا حجیم استفاده کرد. Thread نیز مسائلی مثل مشکلات حافظه و کمبود آن را دارد. از این‌ها نیز بگذریم، کد‌های این ابزار‌ها احتمالا چندان خوشایند نیست و همچنان که از Callback‌های بیشتری استفاده کنید، کد‌های شما ظاهری زشت‌تر را به خود خواهند دید و این شما را حتما آزرده خاطر خواهد کرد.

بعد از اولین تست کاتلین کوروتین، نظر شما درباره همه چیز تغییر خواهد کرد و از کار با آن لذت خواهید برد. کاتلین کوروتین بسیار سبک است و کار با آن بسیار آسان است. از این ابزار در بسیاری از فریمورک‌ها مانند فریمورک Ktor استفاده شده است (این فریمورک بر اساس این ابزار توسط Jetbrains توسعه داده شده است). گوگل نیز برای توسعه UI اندروید با Jetpack Compose از این قابلیت استفاده می‌کند و دلیل آن نیز سبک بودن و سادگی استفاده بیان شده است.

معمولا در زبان‌های برنامه‌نویسی برای اینکه به صورت موازی یک کد را اجرا کنند، از Thread استفاده می‌شود اما چیزی که کاتلین معرفی کرده، شبیه Thread عمل می‌کند اما Thread نیست! زیرا ممکن است چندین جاب از coroutine در یک ترد اجرا شوند و ترد جدیدی برای هر کدام ساخته نشود. همچنین اجرای بیش از حد ترد‌ها و انجام کار موازی ممکن است در نهایت به ایجاد مشکل در پاسخگویی دستگاه شود اما coroutine باعث ایجاد چنین مشکلی در صورتی که حجم کار نیز سنگین باشد، نمی‌شود و حافظه را بسیار بهتر مدیریت می‌کند. آیا زبان‌های دیگر چنین قابلیتی را مثل کاتلین تعبیه کرده‌اند؟ تا کنون من که ندیده‌ام. حقیقتا بسیار تاثیر برانگیز است.

 

پیش‌نیاز‌های استفاده از Coroutines

شاید این دوره مناسب شما باشد: دوره آموزش رایگان اندروید استودیو با کاتلین

 

نصب Coroutines در کاتلین به کمک Dependency

برای نصب Coroutines در کاتلین باید این وابستگی (dependency) را به پروژه خود اضافه کنید.

dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0" }
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0") }

 

برای نصب coroutines در اندروید این وابستگی را نیز اضافه کنید:

dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0" }
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0") }
مطمئن شوید که برای دانلود Kotlin Coroutines حتما مخزن (Repository) میون سنترال (MavenCentral) را به پروژه خود افزوده کرده باشید.

 

اولین مثال از Coroutine

همانطور که گفته شد، یک کوروتین نوعی Suspend است و تقریبا شبیه ترد است اما در واقع ترد نیست و به یک ترد محسور نمی‌شود. ممکن است یک کروتین در یک ترد مکث کند ولی در تردی دیگر دوباره از سر گرفته شود. برای فهم بهتر آن، یک کوروتین را از نظر کارایی به شکل یک ترد ببینید اما بدانید که یکی نیستند و تفاوت‌های بنیادینی با هم دارند.به کد زیر دقت کنید:

 

fun main() = runBlocking { // this: CoroutineScope launch { // launch a new coroutine and continue delay(1000L) // non-blocking delay for 1 second (default time unit is ms) println("World!") // print after delay } println("Hello") // main coroutine continues while a previous one is delayed }

خروجی کد بالا به این شکل است:

Hello
World!

حال بگذارید کد بالا را با هم تحلیل کنیم.

  • launch یک سازنده coroutine است و همزمان با بقیه کد به صورت موازی اجرا می‌شود ولی به صورت مستقل و مجرا. به همین دلیل ابتدا Hello اجرا شد.
  • delay یک متد ویژه برای ایجاد تاخیر در اجرا به اندازه یک زمان مشخص است. ذکر این نکته حائز اهمیت است که delay بقیه کوروتین‌ها در ترد را متوقف نمی‌کند. فقط همان کوروتین متوقف می‌شود و اگر در همان ترد تعداد دیگری کروتین در حال اجرا باشند، بدون ایجاد خللی در آن‌ها به کار خود ادامه می‌دهند.
  • runBlocking هم یک سازنده coroutine است که یک پلی بین کروتین و fun main() می‌باشد. به این معنا که در اینجا می‌توانید کد‌های دو دنیای اپ اصلی و کروتین را بنویسید و ارتباط آن‌ها را با هم تشکیل دهید (همانند runOnUIThread در اندروید). اگر runBlocking را حذف کنید، یک ارور در launch دریافت خواهید کرد. زیرا launch فقط باید در یک CoroutineScope تعریف شود.

 

تابع Suspend در Coroutine

همان کد بالا را در نظر بگیرید. بگذارید تمام کد‌های درون launch {...} را خارج کنیم و درون یک تابع قرار دهیم. وقتی قصد استفاده از قابلیت‌های کروتین در یک تابع را داریم، باید قبل از fun کلمه کلیدی suspend را قرار دهیم. suspend fun چیست؟ در واقع این تابع هیچ تفاوتی با توابع دیگر ندارد. تنها تفاوت آن این است که برای اینکه بتوانید از قابلیت‌های کروتین مانند delay استفاده کنید، باید از کلمه کلیدی suspend نیز استفاده کنید.

بنابراین همان کد قبلی ما به شکل زیر خواهد شد:

fun main() = runBlocking { // this: CoroutineScope launch { doWorld() } println("Hello") } // this is your first suspending function suspend fun doWorld() { delay(1000L) println("World!") }

 

سازنده Scope در Coroutine

این قابلیت وجود دارد که Scope خودتان را به کمک coroutineScope بسازید. پس از ساخته شدن، کار آن تا launch شدن تمامی فرزندان ادامه خواهد یافت و تمام نمی‌شود. شاید با خود فکر کنید که runBlocking و coroutineScope مثل هم عمل می‌کنند اما در واقع تفاوتی بین آن‌هاست. runBlocking باعث می‌شود که ترد متوقف شده و منتظر بماند اما coroutineScope تنها باعث توقف خود کروتین می‌شود و کروتین‌های دیگر در ترد می‌توانند کار خود را ادامه دهند. به همین دلیل runBlocking یک تابع معمولی است ولی coroutineScope یک تابع suspend است.

شما می‌توانید از coroutineScope در هر تابعی از suspend استفاده کنید. برای مثال می‌توانید کد قبل را به این شکل نیز بنویسید:

fun main() = runBlocking { doWorld() } suspend fun doWorld() = coroutineScope { // this: CoroutineScope launch { delay(1000L) println("World!") } println("Hello") }

 

همزمانی و سازنده Scope

یک سازنده coroutineScope می‌تواند در هر تابعی از suspend استفاده شود که بتواند تمام عملیات‌های همزمانی را انجام دهد. بگذارید دو کروتین را به صورت همزمان اجرا کنیم و نتیجه آن را بررسی کنیم:

// Sequentially executes doWorld followed by "Done" fun main() = runBlocking { doWorld() println("Done") } // Concurrently executes both sections suspend fun doWorld() = coroutineScope { // this: CoroutineScope launch { delay(2000L) println("World 2") } launch { delay(1000L) println("World 1") } println("Hello") }

هر دو کروتین در یک coroutineScope به صورت همزمان اجرا می‌شوند. بنابراین World1 به این دلیل که یک ثانیه تاخیر دارد، زودتر از World2 که با تاخیر دو ثانیه‌ای است، اجرا می‌شود. Hello نیز به این دلیل که در هیچ لانچی قرار ندارد و تاخیری نیز شامل آن نشده، بنابراین زودتر از هر دو عبارت اجرا می‌شود. چون runBlocking اجرا شده است، ابتدا باید تمام کروتین‌ها اجرا شوند و پس از اتمام کار آن‌ها، Done نیز چاپ خواهد شد. بنابراین در نتیجه تمامی توضیحات خروجی به شکل زیر خواهد شد:

Hello

World 1

World2

Done

 

جاب (Job) در Coroutine

یک سازنده کروتین launch یک Job را برمی‌گرداند و در واقع یک هندلی برای کروتین است و می‌تواند تا زمان اجرای کامل خود صبر کند. برای مثال می‌توانید منتظر اجرای فرزند باشید و سپس Done را چاپ کنید:

val job = launch { // launch a new coroutine and keep a reference to its Job delay(1000L) println("World!") } println("Hello") job.join() // wait until child coroutine completes println("Done")

خروجی کد بالا این است:

Hello

World!

Done

 

کروتین‌ها سبک هستند

کد زیر را اجرا کنید:

//sampleStart fun main() = runBlocking { repeat(100_000) { // launch a lot of coroutines launch { delay(5000L) print(".") } } } //sampleEnd

این کد صد هزار کروتین را اجرا می‌کند و پس از 5 ثانیه هر کروتین یک نقطه را چاپ می‌کند. کافیست برای مقایسه Thread با Coroutine، کد runBlocking را حذف کنید و به جای launch، از thread استفاده کنید. به جای delay نیز Thread.sleep را قرار دهید. چه اتفاقی خواهد افتاد؟ به نظر تنها یک اتفاق محتمل است و آن نیز کمبود حافظه است اما کروتین‌ها به راحتی این گونه مسائل را مدیریت خواهند کرد و از حافظه به خوبی استفاده می‌کنند. تاثیر برانگیز است نه؟ ساده و کم حجم!

ارسال نظر :
پاسخ به