Coroutine در کاتلین — بخش دوم: تابع های تعلیقی

آخرین به‌روزرسانی: ۲۲ بهمن ۱۳۹۸
زمان مطالعه: ۷ دقیقه

در بخش قبلی این راهنما با مفاهیم مقدماتی کوروتین‌ها در زبان کاتلین آشنا شدیم. در این بخش بر روی مفهوم «تابع های تعلیقی» (Suspending Functions) متمرکز می‌شویم. برای مطالعه بخش قبلی روی لینک زیر کلیک کنید:

تابع های تعلیقی

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

منظور از تابع تعلیقی چیست؟

تابع تعلیقی را می‌توان مانند یک تابع معمولی در نظر گرفت که می‌تواند مکث یابد و پس از اجرای وظیفه‌اش از سر گرفته شود. یعنی می‌توانیم یک وظیفه زمان‌گیر را در تابع قرار دهیم و منتظر بمانیم تا کامل شود. به همین دلیل است که کوروتین‌ها را باید به روش ترتیبی بدون استفاده از callback یا RxJava بنویسیم.

تابع های تعلیقی

تابع‌های تعلیقی می‌توانند صرفاً از یک کوروتین مورد استفاده قرار گیرند. یک تابع تعلیق یافته می‌تواند به صورت یک تابع معمولی مورد استفاده قرار گیرد و اجرای کوروتین را تعلیق خواهد کرد. برای نمونه delay() یک تابع تعلیقی داخلی است. به لطف یادآوری اندروید استودیو با توجه به آیکون فلش در سمت چپ پنل متوجه می‌شویم که این یک تابع تعلیقی است. زمانی که delay(1_000) را در کوروتین فرا می‌خوانیم، اجرا را به مدت 1 ثانیه بدون مسدودسازی نخ تعلیق می‌کند و سپس به کوروتین بازمی‌گردد تا تابع doSomething() را اجرا کند.

برای این که یک تابع تعلیقی را خودمان تعریف کنیم باید از مادیفایر suspend بهره بگیریم. آیا کافی است suspend را به تابع معمول خود اضافه کنیم تا وظایف سنگین را که نخ را مسدود می‌سازند به تابع غیر مسدودکننده تبدیل کنیم؟ پاسخ منفی است. با این که مستندات رسمی اشاره می‌کنند: «تابع تعلیقی می‌تواند اجرای کد را بدون مسدود ساختن نخ جاری و با فراخوانی تابع‌های تعلیقی دیگر معلق سازد.» اما همچنان باید به Dispatchers که تابع‌های تعلیقی را با استفاده از آن اجرا می‌کنیم توجه داشته باشیم.

اگر suspend را روی یک تابع نرمال قرار داده‌اید در این صورت IDE هشداری به صورت استفاده مکرر از مادیفایر suspend ارائه می‌کند.

// IDE warning: "Redundant 'suspend' modifier".
private suspend fun doSomething() {
    // do some heavy tasks
}

ساده‌ترین و درست‌ترین روش این است که وظیفه مورد نظر را درون یک ()withContext قرار دهیم و dispatcher-های مورد نیاز را تعریف کنیم. برای نمونه اگر وظیفه سنگینی با محاسبات مرتبط است باید آن را درون withContext(Dispatchers.default) قرار دهیم.

private suspend fun doSomething() {
    withContext(Dispatchers.Default) {
        // do some heavy tasks
    }
}

برای استفاده از تابع‌های تعلیقی چندین روش وجود دارد که در ادامه آن‌ها را توضیح خواهیم داد.

فراخوانی تابع‌های مسدودکننده از تابع‌های تعلیقی

قرار دادن وظایف زمان‌بر در یک تابع تعلیقی ایده مناسبی محسوب می‌شود. برای نمونه وظیفه مرتبط با شبکه مانند واکشی داده‌های کاربر و به‌روزرسانی UI یک کار متداول است که معمولاً باید انجام دهیم. بزرگ‌ترین مشکل این است که این نوع از وظایف سنگین موجب مسدود شدن نخ اصلی اندروید می‌شوند. برای جلوگیری از توقف اپلیکیشن (ANR) چنین وظایفی را در نخ پس‌زمینه قرار می‌دهیم. مشکل بعدی این است که به‌روزرسانی UI در نخ پس‌زمینه مجاز نیست و از این رو باید از Activity.runOnUiThread(Runnable) و یا حتی Handler به این منظور بهره بگیریم.

به نظر می‌رسد که انجام این کار برای توسعه‌دهندگان اندروید به این روش چندان آسان نباشد. خوشبختانه کوروتین‌های کاتلین در اینجا به کمک ما می‌آیند:

MainScope().launch {
  val user = fetchUser() // Waits for fetching user in I/O thread.
  updateUser(user) // Updates UI in main thread.
}

private suspend fun fetchUser(): User = withContext(Dispatchers.IO) {
  fetchUserFromServerBlocking()
}

private fun fetchUserFromServerBlocking(): User {
  // Do I/O execution.
}

private fun updateUser(user: User) {
  // Updates UI with [User] data.
}

class User

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

Callback و SuspendCancellableCoroutine

فرض کنید از قبل یک پروژه آنلاین اندروید داریم. در این پروژه از وظایف ناهمگام زیادی برای انتظار جهت خواندن پایگاه داده یا واکشی کردن داده‌ها از سرور استفاده شده است. استفاده از تابع‌های Callback یک روش احتمالی برای مدیریت داده‌ها روی نخ اصلی است. بنابراین سؤال این است که چگونه می‌توان این تابع‌های Callback را به کوروتین‌ها تبدیل کرد؟ در اینجا باید از SuspendCancellableCoroutine استفاده کنیم.

SuspendCancellableCoroutine یک CancellableContinuation بازگشت می‌دهد که از resume, resumeWithException استفاده می‌کند و در صورتی که اجرا لغو شود یک استثنای CancellationException صادر می‌کند. تابع مشابه دیگری به نام suspendCoroutine نیز وجود دارد که تنها تفاوت آن این است که suspendCoroutine نمی‌تواند از سوی ()Job.cancel لغو شود.

CancellableContinuation

ما می‌توانیم یک بلوک را در suspendCancellableCoroutine با استفاده از آرگومان CancellableContinuation اجرا کنیم. سه روش برای استفاده از CancellableContinuation وجود دارند:

1. resume(value: T)

این دستور اجرای کوروتین متناظر را با ارسال مقدار بازگشتی آخرین نقطه تعلیق از سر می‌گیرد.

MainScope().launch {
  try {
    val user = fetchUser()
    updateUser(user)
  } catch (exception: Exception) {
    // Use try-catch or CoroutinesExceptionHandler to handle exceptions.
  }
}

// Fetches the user data from server.
private suspend fun fetchUser(): User = suspendCancellableCoroutine { 
cancellableContinuation ->
  fetchUserFromNetwork(object : Callback {
    override fun onSuccess(user: User) {
      // Invokes this line since the callback onSuccess() is invoked.
      cancellableContinuation.resume(user)
    }

    override fun onFailure(exception: Exception) {
      cancellableContinuation.resumeWithException(exception)
    }
  })
}

private fun fetchUserFromNetwork(callback: Callback) {
  Thread {
    Thread.sleep(3_000)
    
    // Invokes onSuccess() with user data.
    callback.onSuccess(User())
  }.start()
}

private fun updateUser(user: User) {
  // Updates UI with [User] data.
}

interface Callback {
  fun onSuccess(user: User)
  fun onFailure(exception: Exception)
}

class User

در نمونه کد فوق اگر CancellableContinuation.resume(user) را فراخوانی کنیم، تابع fetchUser() مقدار [user] را به val user بازمی‌گرداند.

2. resumeWithException(exception: Throwable)

این دستور اجرای کوروتین متناظر را از سر می‌گیرد به طوری که [exception] درست پس از آخرین نقطه تعلیق مجدداً صادر می‌شود:

MainScope().launch {
  try {
    val user = fetchUser()
    updateUser(user)
  } catch (exception: Exception) {
    // Use try-catch or CoroutinesExceptionHandler to handle exceptions.
    Log.d("demo", "$exception") // Prints "java.io.IOException".
  }
  
  // If we handle exception in try-catch, we can still do something after it.
  doSomething()
}

// Fetches the user data from server.
private suspend fun fetchUser(): User = suspendCancellableCoroutine { 
cancellableContinuation ->
  fetchUserFromNetwork(object : Callback {
    override fun onSuccess(user: User) {
      cancellableContinuation.resume(user)
    }

    override fun onFailure(exception: Exception) {
      // Invokes this line since the callback onFailure() is invoked.
      cancellableContinuation.resumeWithException(exception)
    }
  })
}

private fun fetchUserFromNetwork(callback: Callback) {
  Thread {
    Thread.sleep(3_000)
    
    // Invokes onFailure() callback with "IOException()".
    callback.onFailure(IOException())
  }.start()
}

private fun updateUser(user: User) {
  // Updates UI with [User] data.
}

interface Callback {
  fun onSuccess(user: User)
  fun onFailure(exception: Exception)
}

class User

در کد نمونه فوق زمانی که CancellableContinuation.resumeWithException(user) را فراخوانی می‌کنیم، تابع ()fetchUser اقدام به صدور [exception] می‌کند. بدین ترتیب updateUser(user) نمی‌تواند فراخوانی شود و به جای آن try-catch به مدیریت استثنا می‌پردازد. سپس کد پس از بلوک try-catch به صورت پیوسته اجرا می‌شود.

3. cancellableContinuation.cancel()

با این که «استثناهای بررسی شده» (checked exceptions) ندارد، اما باید همه استثناهای ممکن را همچنان در try-catch مدیریت کنیم. در غیر این صورت اپلیکیشن از کار می‌افتد. اما یک استثنای خاص هست که CancellationException نام دارد و زمانی صادر می‌شود که ()cancellableContinuation.cancel را فراخوانی کنیم.

MainScope().launch {
  try {
    val user = fetchUser()
    updateUser(user)
  } catch (exception: Exception) {
    // Handles exceptions here.
    // Prints "java.util.concurrent.CancellationException: Continuation 
    // CancellableContinuation(DispatchedContinuation[Main, Continuation at 
    // com.mutant.coroutinestest.MainActivity$onCreate$1.invokeSuspend
    // (MainActivity.kt:22)@5af0f84]){Active}@65c036d was cancelled normally".
    Log.d("demo", "$exception")
  }
  
  // If we handle exception in try-catch, we can still do something after it.
  doSomething()
}

// Fetches the user data from server.
private suspend fun fetchUser(): User = suspendCancellableCoroutine { 
cancellableContinuation ->
  fetchUserFromNetwork(object : Callback {
    override fun onSuccess(user: User) {
      cancellableContinuation.resume(user)
    }

    override fun onFailure(exception: Exception) {
      cancellableContinuation.resumeWithException(exception)
    }
  })
  
  // We call "contiuation.cancel()" to cancel this suspend function.
  cancellableContinuation.cancel()
}

private fun fetchUserFromNetwork(callback: Callback) {
  Thread {
    Thread.sleep(3_000)
    callback.onSuccess(User())
  }.start()
}

private fun updateUser(user: User) {
  // Updates UI with [User] data.
}

interface Callback {
  fun onSuccess(user: User)
  fun onFailure(exception: Exception)
}

class User

حتی زمانی که این استثنا را مدیریت نکنید، موجب بروز کرش می‌شود. اما کد زیر پس از آن اجرا نخواهد شد:

MainScope().launch {
  val user = fetchUser()
  updateUser(user)
  
  // If we dont't handle CancellationException, this job would be cancelled.
  canNOTBeExecuted()
}

// Fetches the user data from server.
private suspend fun fetchUser(): User = suspendCancellableCoroutine { 
cancellableContinuation ->
  fetchUserFromNetwork(object : Callback {
    override fun onSuccess(user: User) {
      cancellableContinuation.resume(user)
    }

    override fun onFailure(exception: Exception) {
      cancellableContinuation.resumeWithException(exception)
    }
  })
  
  // We call "contiuation.cancel()" to cancel this suspend function.
  cancellableContinuation.cancel()
}

private fun fetchUserFromNetwork(callback: Callback) {
  Thread {
    Thread.sleep(3_000)
    callback.onSuccess(User())
  }.start()
}

private fun updateUser(user: User) {
  // Updates UI with [User] data.
}

interface Callback {
  fun onSuccess(user: User)
  fun onFailure(exception: Exception)
}

class User

فراخوانی RxJava از تابع‌های تعلیقی

در صورتی که از RxJava در پروژه خود استفاده کنیم یک کتابخانه به نام kotlinx-coroutines-rx2 وجود دارد که می‌تواند RxJava را به کوروتین تبدیل کند. آن را با کد زیر ایمپورت کنید:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.3.2"

در تصویر زیر همه سازندگان کوروتین را می‌بینید:

تابع های تعلیقی

برای مثال اگر از Single در RxJava استفاده کنیم، ()Single.await به ما کمک می‌کند که RxJava را به suspendCancellableCoroutine تبدیل کنیم.

تابع های تعلیقی

همچنان که در تصویر فوق می‌بینید، ()await حالت موفق را به ()cancellableContinuation.resume و حالت شکست را به ()cancellableContinuation.resumeWithException ارسال می‌کند. در ادامه اقدام به پیاده‌سازی کد دموی خود می‌کنیم:

MainScope().launch {
  CoroutineScope(Dispatchers.Main).launch {
    val user = fetchUserFromServer().await()
    updateUser(user)
  }
}

private fun fetchUserFromServer(): Single<User> =
  Single.create<User> {
    Log.d("demo", "(1) fetchUserFromServer start, ${Thread.currentThread()}")
    Thread.sleep(3_000)
    it.onSuccess(User())
    Log.d("demo", "(2) fetchUserFromServer onSuccess, ${Thread.currentThread()}")
  }.subscribeOn(Schedulers.io())
      .observeOn(AndroidSchedulers.mainThread())

private fun updateUser(user: User) {
  Log.d("demo", "(3) updateUser, ${Thread.currentThread()}")
}

class User

لاگ خروجی به صورت زیر است:

D/demo: (1) fetchUserFromServer start, Thread[RxCachedThreadScheduler-1,5,main]
D/demo: (2) fetchUserFromServer onSuccess, Thread[RxCachedThreadScheduler-1,5,main]
D/demo: (3) updateUser, Thread[main,5,main]

کد ()fetchUserFromServer().await موجب تعلیق کوروتین می‌شود و تا زمانی که RxJava نتیجه را بازگشت دهد صبر می‌کند. حال سؤال این است که اگر Single در RxJava شکست بخورد و یک استثنا صادر شود چه می‌شود؟

CoroutineScope(Dispatchers.Main).launch {
  try {
    val user = fetchUserFromServer().await()
    updateUser(user)
  } catch (e: Exception) {
    Log.d("demo", "(4) {$e}, ${Thread.currentThread()}")
  }
}

private fun fetchUserFromServer(): Single<User> =
  Single.create<User> {
    Log.d("demo", "(1) fetchUserFromServer start, ${Thread.currentThread()}")
    Thread.sleep(3_000)
    it.onError(IOException())
    Log.d("demo", "(2) fetchUserFromServer onError, ${Thread.currentThread()}")
  }.subscribeOn(Schedulers.io())
      .observeOn(AndroidSchedulers.mainThread())

private fun updateUser(user: User) {
  Log.d("demo", "(3) updateUser, ${Thread.currentThread()}")
}

class User

در این حالت استثنا در try-catch مدیریت می‌شود. لاگ به صورت زیر است:

D/demo: (1) fetchUserFromServer start, Thread[RxCachedThreadScheduler-1,5,main]
D/demo: (2) fetchUserFromServer onError, Thread[RxCachedThreadScheduler-1,5,main]
D/demo: (4) {java.io.IOException}, Thread[main,5,main]

همانند Maybe ،bservable در RxJava تابع‌های اکستنشن متناظری برای استفاده در این حالت وجود دارند. فهرست آن‌ها به شرح زیر است:

تابع های تعلیقی

سخن پایانی

به این ترتیب به پایان این مقاله می‌رسیم، امیدواریم مطالعه این راهنما به افزایش دانش شما در مورد تابع‌های تعلیقی کمک کرده باشد و بدین ترتیب بتوانید آن‌ها را در پروژه‌های خود پیاده‌سازی کنید.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۲ نفر
آیا این مطلب برای شما مفید بود؟
شما قبلا رای داده‌اید!
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
swlh

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *