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.Default
、Dispatchers.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 包 ScheduledExecutorService
的 schedule()
來調度了 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 這種類似的思路。