آموزش جامع کروتنیز (Coroutines) در کاتلین به همراه مثال

آموزش جامع کروتنیز (Coroutines) در کاتلین به همراه مثال

تعریف کروتینز

کروتینز (Coroutines) یک کتابخونه ی مدرن چندنخی در کاتلین است که با چندنخی در جاوا طراحی شده، با کروتنیز در کاتلین کد ها میتونن به صورت زنجیره وار نوشته بشن و به صورت همزمان اجرا بشن.

در Thread برای ایجاد هر کاری باید یک Thread ایجاد کنیم اما در کروتینز، چند کروتین میتونن از یک Thread استفاده کنن و این ویژگی باعث میشه نخ های کمتری ایجاد بشه و باعث بهینه تر شدن برنامه میشه.

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

در زیر عملکرد کروتین با Thread مقایسه شده است.

مقایسه کروتینز (Coroutines) با Thread
مقایسه کروتینز (Coroutines) با Thread

افزودن کروتینز به پروژه

برای اضافه کردن کروتینز به پروژه کافیه در فایل build.gradle به صورت زیر اضافه و سپس پروژه رو سینک کنید.

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:version")    
    //implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:version")
    //Other dependencies
}
          

اگه بخوایم از کروتینز در اندروید استفاده کنیم علاوه بر kotlinx-coroutines-core باید kotlinx-coroutines-android رو هم از کامنت در بیاریم.

توجه

بجای version از آخرین نسخه ی coroutines-core و coroutines-android استفاده کنید.

کلیدواژه suspend

با استفاده از کلیدواژه suspend میتونیم یک تابع تعلیق پذیر (suspend function) تعریف کنیم، تابع ساسپند میتونه به صورت موقت متوقف بشه و سپس ادامه پیدا کنه؛ توابع ساسپند فقط داخل کروتین یا داخل تابع ساسپند دیگه قابل صدا زدن هستن.

فرم کلی

suspend fun mySuspendFunction(){
  //Execute codes here
}
          

یکی از راه های صدا زدن توابع ساسپند داخل تابع main، تعریف تابع main به عنوان تابع ساسپند است.

suspend fun main(){
  //start point
  mySuspendFunction()
}

suspend fun mySuspendFunction(){
  println("Hello from suspend function!")
}
          

محدوده کروتین

برای کروتین هایی که ایجاد میکنیم باید محدوده تعیین کنیم، برای ایجاد محدوده یکی از توابع coroutineScope یا CoroutineScope رو صدا میزنیم.

با محدوده ی کروتین چرخه ی حیاط (lifecycle) کروتین ها رو مدیریت میکنیم.

تابع coroutineScope

تابع coroutineScope یک تابع ساسپند (suspend) است، این تابع کانتکست کروتین Parent رو به ارث می بره و مقداری از نوع R رو بر میگردونه.

suspend fun mySuspendFunction(){
  val scope = coroutineScope{
    //scope.launch{}
  }
}
            

تابع CoroutineScope

زمانی که بخوایم یک محدوده با کانتکست (Context) اختصاصی ایجاد کنیم از تابع CoroutineScope استفاده می کنیم.

fun myFunction(){
  val scope = CoroutineScope(context)
  //scope.launch{}
}
            

بیلدر (Builder) ها و ایجاد کروتین با بیلدر (Builder) ها

با استفاده از توابع launch, async و runBlocking میتونیم کروتین جدید ایجاد کنیم؛ این سه تابع به علاوه تابع withContext که در قسمت های بعدی این مطلب بهش می پردازیم به بیلدر (Builder) معروف هستن.

تابع launch

تابع launch یک تابع اکستنشن برای CoroutineScope است،‌ با تابع launch میتونیم یک کروتین ایجاد کنیم و سپس کروتین ایجاد شده به صورت خودکار اجرا میشه، این تابع یک نمونه از Job رو برمیگردونه؛ تابع launch معروفه به fire and forget، زمانی که کروتین با این تابع اجرا میشه کد های داخلش اجرا شده و مقداری رو بر نمی‌گردونه.

فرم کلی:

val job: Job = launch{
  //Execute codes here
}
              

تابع async

تابع async یک تابع اکستنشن برای CoroutineScope است؛‌ و باهاش میتونیم مقداری از نوع R رو برگردونیم؛ زمانی که این تابع رو صدا میزنیم،‌ کروتینز اجرا نمیشه و فقط یک نمونه از Deferred رو بر میگردونه.

اینترفیس Deferred از Job ارث‌بری میکنه و تمام ویژگی ها و کلاس های Job رو به همراه داره.

با صدا زدن تابع await در اینترفیس Deferred، تابعی که توش صدا زده میشه رو ساسپند‌(معلق) میکنه و کروتین async به اجرا در میاد و پس از اجرای تابع async و برگردوندن نتیجه از طریق await، تابعی که await داخلش صدا زده شده ادامه پیدا می کنه.

فرم کلی:

val deferred: Deferred<T> = async{  

  val r: R = ...
  
  return@async r
}

val result: R = deferred.await()
              

ایجاد کروتین با launch و async

با استفاده از دو تابع launch و async میتونیم کروتین جدید ایجاد کنیم، به سه طریق میتونیم این دو تابع رو صدا بزنیم.

با استفاده از نمونه ی ایجاد شده از CoroutineScope میتونیم async و launch رو صدا بزنیم.

val scope = CoroutineScope(context)

scope.launch{
  //Execute codes here
}

scope.async{
  //Execute codes here
}          

میتونیم داخل بلوک مربوط به CoroutineScope تابع async و launch رو صدا بزنیم.

coroutineScope{
  launch{
    //Execute codes here
  }

  async{
    //Execute codes here
  }
}
              

میتونیم داخل خودشون، خودشونو صدا بزنیم.

coroutineScope{
  launch{
    //Execute codes in outer coroutine
    val deferred = async{
      //Execute codes inside async
    }
  }
}
              

ایجاد کروتین با runBlocking

میتونیم با استفاده از تابع runBlocking در هرجا که خواستیم کروتین ایجاد کنیم، تابع runBlocking روی هر نخی که صدا زده بشه اون نخ رو تا پایان اجرا مسدود میکنه.

توجه

استفاده از تابع runBlocking کمتر توصیه میشه و معمولا به عنوان تابع main برای تست و اجرای کروتین ازش استفاده میکنن.

fun main() = runBlocking{
  //Execute codes here
}
          

در مثال زیر میخوایم برنامه n بار یک کاراکتر رو نمایش بده.

fun printChars(char: Char, numberOfChars: Int){
  
  val scope = CoroutineScope(Dispatchers.Default)

  scope.launch {

    println("Printing $numberOfChars $char: ")

    for (i in 0 until numberOfChars) {
        if (i % 10 == 0) println()
          print(char)
        }
        println()
    }
}            
          

در مثال زیر میخوایم برنامه اعداد یک تا n رو نمایش بده.

fun printNumbers(n: Int){

    val scope = CoroutineScope(Dispatchers.Default)

    scope.launch {

        for (i in 1..n){
            if (i % 10 == 0) println()
            print("$i ")
        }

    }
}
          

صدا زدن دو تابع printNumbers و printChars

fun main() {
  printChars()
  printNumbers()
}
          

در مثال زیر برنامه یک متن چاپ می کنه و با تابع async حاصل جمع اعداد 1 تا 100 رو نمایش میده و در نهایت پیغام آخر رو چاپ می کنه.

suspend fun main(){
  helloAndSum()
}

suspend fun helloAndSum(){
  println("Hello from suspend function!")

  val result = coroutineScope{
    val deferred = async{

      var sum = 0

      for(i in 1 .. 100){
        sum += i
      }

      return@async sum
    }

    val asyncResult = deferred.await()

    asyncResult
  }

  println("Sum of 1 to 100 is $result")

  println("Goodbye!")
}
          

در زیر میخوایم اعداد یک تا 100 رو نمایش بدیم و زمانی که عدد بر 10 بخش پذیر باشه جمع اعداد یک تا اون عدد رو با async محاسبه کنیم.

suspend fun printNumbersAndSumDivisibleOfTens(){

    for (i in 1 .. 100){

        print("$i ")

        if (i % 10 == 0){

            println()

           val sum = coroutineScope {
                val deferred: Deferred = async {
                    var sum = 0

                    for (k in 1..i) {
                        sum += k
                    }

                    return@async sum
                }

                val sum = deferred.await()

                sum
            }
            
            println("1-$i sum:  $sum")
        }
    }
}
              

کروتین های تودرتو

داخل هر Coroutine میتونیم یک کروتین جدید ایجاد کنیم؛ به کروتین هایی که داخل یک کروتین دیگه ایجاد میشن، کروتین فرزند (child) و به کروتینی که داخلش ایجاد شدن کروتین parent میگیم.

برای ایجاد کروتین جدید داخل یک کروتین دیگه نیازی به تعریف CoroutineScope نداریم.

val scope = CoroutineScope(context)

scope.launch{

  launch{
    
    //Execute codes here

    launch{
      //Execute codes here
    }

  }

  launch{
    //Execute codes here
  }
  
}
            

بررسی کانتکست (Context)

برای هر CoroutineScope و بیلدر (Builder) یک Context وجود داره.

کانتکست (Context) در کروتین چهار ويژگی داره که عبارتند از Dispatcher, Job, CoroutineName و CoroutineExceptionHandler؛ برای ایجاد کانتکست در کروتین میتونیم یک دو یا هر چهار ويژگی رو با علامت اختصاصی + تعریف کنیم.

بجز Job تمام ویژگی های context به صورت پیشفرض از parent به child می رسه مگه اینکه بخوایم تغییرشون بدیم.

val context = Dispatchers.Default + Job() + CoroutineName("my-coroutine") + CoroutineExceptionHandler {context, throwable -> }

val scope = CoroutineScope(context)
scope.launch{

}
          

اعزام کننده (Dispatcher)

با Dispatcher نخ هایی که کروتین میخواد روشون اجرا بشه رو تعیین میکنیم؛ در کاتلین به صورت پیشفرض چهار دیسپچر, Main, Default, IO و Unconfined وجود داره که میتونیم یکیشونو با کلاس Dispatchers صدا بزنیم.

Dispatchers.Main

با Dispatchers.Main میتونیم مشخص کنیم کروتین مورد نظر روی نخ اصلی اجرا بشه.

این Dispatcher فقط با kotlinx-coroutines-core قابل استفاده نیست معمولا به همراه کتابخونه های جانبی مانند kotlinx-coroutines-android, kotlinx-coroutines-swing, kotlinx-coroutines-javafx و ... قابل استفاده است.

Dispatchers.Default

با Dispatchers.Default کروتین روی یک Thread Pool مشترک اجرا میشه، معمولا از این دیسپچر برای کارهایی که نیاز به پردازش سنگین دارن استفاده میکنیم.

میخوایم یک کروتین با Dispatchers.Default ایجاد کنیم.

val context = Dispatchers.Default
val scope = CoroutineScope(context)
scope.launch{

}
              

Dispatchers.IO

زمانی که بخوایم عملیات خوندن و نوشتن فایل ها رو انجام بدیم از Dispatchers.IO استفاده می کنیم.

val context = Dispatchers.IO
val scope = CoroutineScope(context)
              

Dispatchers.Unconfined

روی نخی که کروتین صدا زده شده اجرا میشه، در صورت تعلیق، هنگام ادامه دادن روی نخی که ادامه پیدا میکنه اجرا میشه.

در مثال زیر ابتدا روی Main اجرا میشه و بعد از صدا زدن تابع delay روی DefaultExecutor که تابع delay روش اجرا شده ادامه پیدا میکنه.

fun main() = runBlocking{
  coroutineScope{
    launch(Disptachers.Unconfined){
      println("Unconfined: Running on ${Thread.currentThread().name}")
      delay(100L)
      println("Unconfined: Running on ${Thread.currentThread().name}")
    }
  }
}
              

ایجاد Dispatcher اختصاصی

میتونیم با استفاده از دو تابع newSingleThreadContext و newFixedThreadPoolContext دیسپچر اختصاصی تعریف کنیم.

در زیر میخوایم یک Dispatcher اختصاصی تعریف کنیم و در کروتین ازش استفاده کنیم و سپس یک کروتین دیگه با Disptachers.Default داخلش تعریف کنیم.

@OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
val Dispatchers.myDispatcher: CoroutineDispatcher
    get() = newSingleThreadContext("myThread")

val context = Disptachers.myDisptacher

val scope = CoroutineScope(context)

scope.launch{
  
  //Execute codes here

  launch(Disptachers.Default){

  } 

}
              

Job

برای کانتکست (Context) هر کروتین یک Job تعریف میشه، با Job میتونیم کروتین رو کنترل کنیم.

در نمودار uml زیر توابع و متد های رایج Job شرح داده شده است.

kotlinx.coroutines.Job
+cancel() کروتین مربوط به Job رو کنسل می‌کنه؛ اگه کروتین parent کروتین های دیگه باشه کروتین های child نیز به صورت بازگشتی کنسل میشن.
+join() هنگامی که در کروتین parent صدا زده بشه، parent تا پایان کار child متوقف میشه.
+isActive: Boolean اگه کروتین فعال باشه true رو بر میگردونه و اگه کنسل شده باشه یا به پایان رسیده باشه false رو بر میگردونه.
+children: Sequence<Job> یک زنجیره از کروتین های فرزند رو بر میگردونه.

ایجاد کروتین بدون تعریف آشکار Job

val scope = CoroutineScope(Disptachers.Default)
scope.launch{
  //Execute codes in parent
  launch{
    //Execute codes in child
  }
}
            

به صورت پیشفرض برای هر کروتین یک Job ایجاد میشه، اما میتونیم با استفاده از تابع Job برای هر کروتین یک Job به صورت آشکار تعریف کنیم.

در زیر به صورت آشکار مشخص کردیم که در مثال قبل Job چطور تعریف شده.

val scope = CoroutineScope(Dispatchers.Default)

val parent: Job = Job()

scope.launch(parent){
  //Execute codes in parent
  val child: Job = Job(parent)
  launch(child){
    //Execute codes in child
  }
}
            

به صورت پیشفرض Job هر کروتین وابسته به Job کروتین parent است (در صورت وجود parent)؛ زمانی که parent کنسل بشه children ها هم به صورت بازگشتی کنسل میشن و زمانی که یک اکسپشن برای یکی از children ها رخ بده parent نیز متوقف میشه.

با ایجاد Job به صورت آشکار برای یک کروتین میتونیم بدون مشخص کردن parent یک کروتین مستقل ایجاد کنیم.

val scope = CoroutineScope(Dispatchers.Default)

val parent: Job = Job()
              
scope.launch(parent){
  //Execute codes in parent
  val child: Job = Job()
  launch(child){
    //Execute codes in child
  }
}
            

CoroutineName

با استفاده از CoroutineName برای کروتین مورد نظر اسم تعریف می کنیم.

val context = Dispatchers.Default + CoroutineName("my-coroutine-with-Dispatchers.Default")
val scope = CoroutineScope(context)
            

CoroutineExceptionHandler

با CoroutineExceptionHandler می‌تونیم اکسپشن ایجاد شده در کروتین مورد نظر رو مدیریت کنیم.

val context = Dispatchers.Default + CoroutineName("my-coroutine") + CoroutineExceptionHandler{context, throwable -> /*Handle Exception here*/}

val scope = CoroutineScope(context)

//continue...
            

تابع withContext

با استفاده از تابع withContext می‌تونیم بدون ایجاد کروتین جدید کانتکست (Context) کروتین فعلی رو تغییر بدیم و پس از انجام کار به کروتین فعلی برگردیم.

تابع withContext یک تابع suspend است و باید داخل کروتین یا تابع suspend صدا زده بشه.

با تابع withContext میتونیم یک مقدار از نوع R رو برگردونیم.

در مثال زیر ابتدا اولین پیغام تابع چاپ میشه و سپس با withContext کانتکست عوض میشه و پیغام داخل withContext چاپ میشه و در نهایت به کانتکست کروتین اصلی بر میگردیم و پیام اخر چاپ میشه.

suspend fun testWithContext(){

    println("Before calling withContext...")

    val result: String = withContext(Dispatchers.Default){

        println("Executing codes in withContext...")

        return@withContext "done"
    }


    println("withContext operation is $result")

    println("After calling withContext...")
}
          

مورد مطالعه (مثال) ها

در مثال زیر برنامه یک فایل متنی رو میخونه و تعداد دفعات تکرار کلمه ای که بهش دادیم رو میشماره، آرگومان اول کانستراکتور مسیر فایل متنیه و آرگومان دوم کلمه ایه که میخوایم برنامه بشماره

suspend fun main() {
    val rto = ReadTextOccurrences("path_to_file", "word_name")

    println("${rto.word}: ${rto.readAndCount()} times of occurrences")
}

class ReadTextOccurrences(path: String, var word: String) {

    private val input: Scanner

    init {
        val file = File(path)
        input = Scanner(file)

        word = word.lowercase()
    }

    fun readAndCount(): Int {

        var occurrences = 0

        val scope = CoroutineScope(Dispatchers.Default)

        val job = scope.launch {

            val text = StringBuilder()

            withContext(Dispatchers.IO) {
                while (input.hasNextLine()) {
                    val line = input.nextLine()
                    text.append(line).append("\n")
                }

                input.close()
            }

            val words = text.split(Regex("\\s"))

            for (w in words) {
                if (w.isNotBlank()) {
                    if (w.equals(word, true)) {
                        println(occurrences)
                        occurrences++
                    }
                }
            }
        }

        while (job.isActive);


        return occurrences
    }
}
          

در بالا از Job.isActive استفاده کردیم تا مطمئن بشیم قبل از پایان کار کروتین برنامه متوقف نمیشه، میتونیم بجای Job.isActive از Job.join استفاده کنیم؛ تابع join کروتین parent رو معلق میکنه تا job مورد نظر به پایان برسه.

در مثال زیر میخوایم فایل json سایت رو بگیریم و بخونیم.

suspend fun main() {

    val scope = CoroutineScope(Dispatchers.Default)

    val deferred = scope.async {

        val uri = URI("https://kodedevel.ir/resources/json/metadata.json")
        
        val url = uri.toURL()

        val content = withContext(Dispatchers.IO) {

            val stream = url.openStream()

            val input = Scanner(stream)

            var content = ""

            while (input.hasNext())
                content = content.plus(input.nextLine())
            
            input.close()

            return@withContext content
        }

        content
    }

    val jsonContent = deferred.await()
    println(jsonContent)

}
          

خلاصه

  • کروتینز یک کتابخونه ی مدرن است که توسط کاتلین و با چندنخی در جاوا ایجاد شده
  • یک نخ میتونه چند کروتین رو اجرا کنه و به همین علت کروتین بهینه تر از چندنخی است.
  • برای ایجاد کروتین ابتدا باید محدوده کروتین رو تعریف کنیم و سپس کروتین ایجاد کنیم.
  • میتونیم با استفاده از سه تابع launch, async و runBlocking کروتین ایجاد کنیم.
  • میتونیم داخل یک کروتین، کروتین دیگه ای ایجاد کنیم و به هرکدوم به ترتیب Parent و Child میگیم.
  • هر کروتین یک کانتکست داره و هر کانتکست چهار ویژگی رایج داره که عبارتند از Disptachers, Job, CoroutineName و CoroutineExceptionHandler
  • با استفاده از کلیدواژه suspend میتونیم یک تابع ساسپند ایجاد کنیم.
  • تابع ساسپند میتونه به صورت موقت متوقف بشه و سپس ادامه پیدا کنه.
  • با استفاده از withContext میتونیم بدون نیاز به ایجاد کروتین جدید، کانتکست کروتین فعلی رو تغییر بدیم و بعد از اتمام کار تابع به کروتین فعلی برگردیم.

بازخورد و دیدگاه‌ ها

اولین نفری باشید که در این صفحه نظر می‌دهید

notifications برای اطلاع از جدیدترین مطالب و پست های مرتبط، عضو کانال تلگرام ما بشید.

arrow_drop_up
کپی شد!