對比 delay() 和 sleep()

Kotlin 語言中的協程 Coroutine 極大地幫助了開發者更加容易地處理異步編程。就 JVM 的角度而言,協程一定程度上減少了 “回調地獄” 的問題,切實地改進了異步處理的編碼方式。Coroutine 中封裝的諸多高效 API,可以確保開發者花費更小的精力去完成并發任務。今天就來對比一下 Coroutine 中的 delay() 和 Java 語言中的 sleep()。

delay()

如果使用過協程,對于 delay() 必然不陌生,先來看一下官方描述:

Delays coroutine for a given time without blocking a thread and resumes it after a specified time. If the given timeMillis is non-positive, this function returns immediately.

delay() 用來延遲協程一段時間,但不阻塞線程,并且能在指定的時間后恢復協程的執行。

用法也很簡單,delay() 是 suspend 函數,直接在 CoroutineScope 里調用即可:

lifecycleScope.launch {
    Log.d(TAG, "1")
    delay(1000)
    Log.d(TAG, "2")
}
lifecycleScope.launch {
    Log.d(TAG, "3")
}

上述代碼創建了兩個協程,且在第一個協程中使用了 delay(),但是這并不影響第二個協程。因此日志輸出結果為:1,3,2,其中1和2兩個日志輸出時間間隔1秒。

總結一下關于 delay() 的特點:

  • 用于延遲當前協程
  • 不會阻塞當前運行的線程
  • 允許其他協程在同線程運行
  • 當延遲的時間到了,協程會被恢復并繼續執行

sleep()

sleep() 是 Java 語言中標準的多線程處理 API:促使當前執行的線程進入休眠,并持續指定的一段時間。該方法一般用來告知 CPU 讓出處理時間給 App 的其他線程或者其他 App 的線程。

如果在協程里使用該函數,它會導致當前運行的線程被阻塞,同時也會導致該線程的其他協程被阻塞,直到指定的阻塞時間完成。

對比 delay() 和 sleep()

假使在單線程里執行并發任務。

下面的代碼分別啟動兩個協程,并各自調用了 1000ms 的 delay() 或 sleep()。

lifecycleScope.launch {
    val totalTime = measureTimeMillis {
        supervisorScope {
            launch {
                Log.d(TAG, "1")
                delay(1000)
//              Thread.sleep(1000)
                Log.d(TAG, "2")
            }
            launch {
                Log.d(TAG, "3")
                delay(1000)
//              Thread.sleep(1000)
                Log.d(TAG, "4")
            }
        }
    }
    Log.d(TAG, "totalTime:$totalTime")
}

當調用 delay() 時,兩個協程在同一時間執行,先輸出日志1和3,經過1秒后,再輸出日志2和4,兩個協程一共花了 1144 ms。

當調用 sleep() 時,先執行第一個協程輸出日志1,經過1秒后,輸入日志2,同時執行第二個協程,輸出日志3,再經過1秒后,輸入之日4,兩個協程一共花了 2152 ms。

這也印證了上面提到的特性差異:delay() 只是掛起當前協程、同時允許其他協程運行該線程,而 sleep() 則在一段時間內直接阻塞了整個線程。

再來看一下 delay() 的其他特點

下面先定義了一個最大創建 2 個線程的線程池 context 示例,然后創建兩個協程,并在第一個協程中使用了 delay(),日志輸出當前線程名字:

val duetContext = newFixedThreadPoolContext(2, "Duet")
runBlocking(duetContext) {
    launch {
        Log.d(TAG, "1-${Thread.currentThread().name}")
        delay(1000)
        Log.d(TAG, "2-${Thread.currentThread().name}")
    }
    launch {
        Log.d(TAG, "3-${Thread.currentThread().name}")
    }
}

我們已經知道日志輸出結果為:1,3,2,其中1和2兩個日志輸出時間間隔1秒。每一個日志輸出的當前線程名字是什么呢?

1-Duet-2
3-Duet-1
2-Duet-1

一開始第一個協程在 delay 函數執行前是運行在Duet-2線程的,但當 delay 完成后,它卻恢復到了另一個線程:Duet-1。這就是 delay() 的另一個特點:協程可以掛起一個 thread 并且恢復到另一個 thread!

delay() 原理

delay() 會先在協程上下文里找到 Delay 的實現,接著執行具體的延時處理。

public suspend fun delay(timeMillis: Long) {
    if (timeMillis <= 0) return // don't delay
    return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
        // if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.
        if (timeMillis < Long.MAX_VALUE) {
            cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
        }
    }
}

Delay 是 interface 類型,其定義了延時之后調度協程的方法 scheduleResumeAfterDelay() 等。開發者直接調用的 delay()、withTimeout() 正是 Delay 接口提供的支持。

 public interface Delay {
     public fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>)

     public fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle =
         DefaultDelay.invokeOnTimeout(timeMillis, block, context)
 }

事實上,Delay 接口由運行協程的各 CoroutineDispatcher 實現。

CoroutineDispatcher 是抽象類,Dispatchers 類會利用線程相關 API 來實現它。比如:

  • Dispatchers.DefaultDispatchers.IO 使用 java.util.concurrent 包下的 Executor API 來實現。
  • Dispatchers.Main 使用 Android 平臺上特有的 Handler API 來實現。

各 Dispatcher 需要實現 Delay 接口,主要就是實現 scheduleResumeAfterDelay() ,去返回指定毫秒時間之后執行協程的 Continuation 實例。

以下是 ExecutorCoroutineDispatcherImpl 類實現該方法的具體代碼:

 override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
     val future = (executor as? ScheduledExecutorService)?.scheduleBlock(
         ResumeUndispatchedRunnable(this, continuation),
         continuation.context,
         timeMillis
     )
     // Other implementation 
 }
 
private fun ScheduledExecutorService.scheduleBlock(block: Runnable, context: CoroutineContext, timeMillis: Long): ScheduledFuture<*>? {
    return try {
        schedule(block, timeMillis, TimeUnit.MILLISECONDS)
    } catch (e: RejectedExecutionException) {
        cancelJobOnRejection(context, e)
        null
    }
}

可以看到借助了 Java 包 ScheduledExecutorServiceschedule() 來調度了 Continuation 的恢復。

Dispatchers.Main 使用 HandlerDispatcher,看一下 HandlerDispatcher 又是如何實現 scheduleResumeAfterDelay 方法的,具體實現在 HandlerContext 里:

 override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
     val block = Runnable {
         with(continuation) { resumeUndispatched(Unit) }
     }
     if (handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY))) {
         continuation.invokeOnCancellation { handler.removeCallbacks(block) }
     } else {
         cancelOnRejection(continuation.context, block)
     }
 }

可以看到直截了當地使用了 Handler 的 postDelayed() post 了 Continuation 恢復的 Runnable 對象。這也解釋了 delay() 沒有阻塞線程的原因。

所以假使在 Android 主線程的協程里執行了 delay() 邏輯,其效果等同于調用了 Handler 的 postDelayed。

這種實現非常有趣:在 Android 平臺上調用 delay(),實際上相當于通過 Handler post 一個 delayed runnable;而在 JVM 平臺上則是利用 Executor API 這種類似的思路。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 在今年的三月份,我因為需要為項目搭建一個新的網絡請求框架開始接觸 Kotlin 協程。那時我司項目中同時存在著兩種...
    業志陳閱讀 1,105評論 0 5
  • 協程屬于Kotlin中非常有特色的一項技術,因為大部分編程語言中是沒有協程這個概念的。 那么什么是...
    隨風cvil閱讀 904評論 0 1
  • 一、Kotlin 協程概念 Kotlin 協程提供了一種全新處理并發的方式,你可以在 Android 平臺上使用它...
    4e70992f13e7閱讀 1,769評論 0 2
  • 在今年的三月份,我因為需要為項目搭建一個新的網絡請求框架開始接觸 Kotlin 協程。那時我司項目中同時存在著兩種...
    Android開發指南閱讀 869評論 0 2
  • 在 Kotlin 中的變量、常量以及注釋多多少少和 Java 語言是有著不同之處的。下面詳細的介紹 Kotlin ...
    馳同學閱讀 935評論 0 2