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

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。