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 delayreturn 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 这种类似的思路。