工作调度
前言
在系统级别组织工作是与多线程和并行计算相关的所有事物的基础。过去,当系统只有一个核心处理器并且只能使用一个线程时,编写优化的组织算法非常重要,这样系统就不会冻结,并且操作也不会永远完成。确定系统需要完成的工作单元的顺序、严重性和资源使用情况的过程称为 调度。就像定期的日程安排一样,它可以安排所有的会议和杂务,它可以最好地组织哪些活动应该在其他活动之前进行。它还处理工作在内存中的位置以及它何时开始或结束——生命周期以及发生异常时的行为。
因此,比如当系统收到需要处理的五个事件时,它首先会查看可用的计算能力。如果它的负载已经低于 70%,那么它就无法承担需要总负载 40% 的任务。然后,它尝试通过在不会使系统超载的其他任务之间分配资源来填充可用的计算能力。但这里有一个警告,如果你继续尝试用较小的任务来填充工作负载,你可能永远无法释放足够的计算能力来最终处理更大的工作单元。
在操作系统中,上述所有的这些工作职责都归 调度器 管理。调度器决定何时以及如何将计算机的资源分配给哪些任务。调度器还负责处理你交给它们的工作的生命周期,因为只有在调度器给系统 开绿灯 之前,事件才会开始,并且在事件完成之前它们也不会结束。如果任何事件中断就会发生异常,调度器就会收到通知,系统就会终止进程。
就协程和现代系统而言,调度通常归结为 线程池 中线程之间的工作分配和组织。它们允许系统将所有职责抽象到一个看似简单的对象中。
了解线程池
线程池 是 汇集 在一起并分布在系统在其队列中接收的工作事件之间的多个线程。今天的硬件支持同时做多件事,并且由于有多个核心,因此可以有效地处理比以前数倍的工作量。再加上协程可以一次完成一个部分,而不是运行整个操作,这可以使协程具有极高的性能。这允许你同时运行多个协程,并以这样一种方式调度线程,即每个线程在每个协程上做一些工作,直到所有工作都完成,同时不断重新分配所有线程。
在内部,这就是线程池的作用所在。就像上面的例子一样,你可以告诉线程池完成五个协程。线程池将会为每个协程分配线程,如果有需要,可以有效地将它们切换出去,如果出现一些优先级更高的工作,比如从应用程序外部触发的重要系统调用,则可以挂起协程。一旦线程再次空闲,它将返回到线程池,系统将再次决定是否有工作要做,或者是否应该等待更多的工作事件。
了解系统如何处理每个线程的状态也很重要。
上下文切换
在 Coroutines API 中,你不必创建自己的线程或线程池,也不必关心如何调度多个协程的执行方式及其生命周期。Coroutines API 有一种特定的方式来传达所有这些信息——通过 ContinuationInterceptors
,你可以通过Dispatchers
提供该接口,你将在本章后面了解该接口。
为了完全理解这些Dispatcher是如何工作的,了解进程和线程状态的底层通信模式是很重要的,这是系统为你准备的。这个模式称为 上下文切换。然而,单任务系统和多任务系统的定义各不相同。但在本章中,我们将重点讨论多任务系统。
本质上,当系统切换上下文时,这意味着它正在一个任务移动到另一个任务,保存上一个任务的状态,以便后续可以恢复它。这听起来很熟悉吧?这与 Continuation
在内部处理可挂起函数中的挂起点时所做的非常相似。这也是为什么所有调度器都实际实现 ContinuationInterceptor
的原因,因为通过拦截 Continuations
的过程及其执行流程,系统可以挂起和恢复当前任务或函数的上下文。
但是暂停和恢复任务并不是全部,系统还应该能够在单个任务中的线程之间切换。仔细想想,这两个概念贯穿整个 Kotlin 协程。
如果你需要在后台做一些事,然后再切换到主线程,发布一个值或操作的某些结果,你最终会创建另一个协程,将其推送到主线程,然后从内部切换到该协程。这就是 上下文切换,此外它还在线程之间切换。因此,每个协程中最重要的部分被称为 CoroutineContext
并非巧合。
现在你已经了解了系统如何处理协程的上下文切换,是时候开始 调度 了!:]
解释 ContinuationInterceptors
尽管本章提到了 ContinuationInterceptors
,但它们是如何工作的可能仍然有点不清楚。如果你从示意图中记得调用堆栈中的函数会发生什么,以及何时调用可挂起函数:
当你在堆栈中有多个函数和多个续体时,你可以通过传播值或异常返回堆栈,一直返回到主续体。
ContinuationInterceptors
与该函数执行和线程一起工作。每次启动协程,或使用 Dispatcher
调用挂起函数时,都会让拦截器有能力暂停和恢复协程的续体——即协程的执行流程。它可以在一个点拦截值的传播,并将其重定向到另一个协程或任务。
因此,如果你使用 Dispatchers.Default
创建一个协程以获得一些值,然后在其中,你可以使用 Dispatchers.Main
再启动一个新的协程,并将其推送到主线程上,这样你将有效地拦截第一个协程的执行,继续在第二个协程中传递上下文和值,以便你可以在主线程上执行一些工作,然后在你完成工作后结束两个协程。如果第二个协程中出现任何问题,拦截器将把异常一直传播到父协程,从而取消两个协程。
这种类型的行为是通过 包装续体 的过程来实现的。每次使用 ContinuationInterceptor
切换上下文时,它都会调用 interceptContContation()
包装上一个 Continuation
来创建一个新的 Continuation
。该函数的签名如下:
abstract fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>
它非常简单,但是也相当强大。每当系统发出函数的续体信号时,无论是新值或异常,ContinuationInterceptor
都可以接受该续体,然后对其进行一些工作,并最终恢复执行。对于调度器来说,ContinuationInterceptors
所做的工作通常是通过从一个线程池切换到另一个线程池来进行上下文切换。