目录
一、调度器应用原因
调度器应用的目的是切换线程,切线程是不希望要切线程的代码挡住我当前的线程,所以需要开一个并行的线程执行这段代码。调度器即确认相关协程在哪个线程上执行,调度的本质是解决挂起恢复后协程逻辑在哪里运行的问题,其继承自拦截器。
并行线程叫做子线程或后台线程。
而不希望被挡住的线程在 Android 中主要代指的主线程,因为界面的更新指定了要在主线程执行;切回主线程执行的代码可以用 handler.post 或 view.post:
val handler = Handler(Looper.getMainLooper())
handler.post {
// 主线程执行代码
}
view.post {
// 主线程执行代码
}
在有 kotlin 后,kotlin 提供了另外一种切线程的方式:协程。
Kotlin调度器(Dispatcher)在Kotlin协程中的应用原因,主要可以从以下几个方面来阐述:
1.简化异步编程
Kotlin协程是一种轻量级的并发编程框架,旨在简化异步编程和多线程操作。调度器作为协程的重要组成部分,通过决定协程在哪个线程或线程池中执行,从而实现了对协程执行流程的精确控制。这使得开发者能够以顺序的方式编写异步代码,而无需显式地管理线程或回调函数,从而大大提高了代码的可读性和可维护性。
2.优化线程管理
调度器能够智能地管理线程资源,避免线程的频繁创建和销毁带来的开销。在Kotlin协程中,调度器可以根据任务的特性和执行环境,选择合适的线程或线程池来执行协程。例如,对于CPU密集型的计算任务,可以使用Dispatchers.Default,它使用共享的线程池来执行协程,从而充分利用多核CPU的计算能力;对于IO密集型的任务,如文件读写、网络请求等,可以使用Dispatchers.IO,它使用专用的线程池来执行IO操作,以避免阻塞CPU。
3.提高应用性能
通过合理使用调度器,可以显著提高应用的性能。例如,在Android应用中,更新UI元素需要在主线程上进行。使用Dispatchers.Main可以将协程调度到主线程中执行,从而确保UI更新的正确性。同时,对于耗时较长的任务,如网络请求或文件读写,可以使用Dispatchers.IO或Dispatchers.Default来执行,以避免阻塞主线程,提高应用的响应性。
4.支持多种应用场景
Kotlin协程提供了多种内置的调度器,如Dispatchers.Default、Dispatchers.IO、Dispatchers.Main等,以及自定义调度器的创建方式,以适应不同的应用场景。例如,Dispatchers.Unconfined可以用于测试或者在知道代码将在正确的上下文中运行时使用;自定义调度器则可以根据特定的需求(如特定的线程池配置)来控制协程的执行。
综上所述,Kotlin调度器在Kotlin协程中的应用原因主要包括简化异步编程、优化线程管理、提高应用性能以及支持多种应用场景。这些优势使得Kotlin协程成为处理并发任务和异步操作的一种强大工具。
二、源码分析
Coroutine使用Dispatchers来负责调度协调程序执行的线程,这一点与RxJava的schedules有点类似,但不同的是Coroutine一定要执行在Dispatchers调度中,因为Dispatchers将负责resume被suspend的任务。
如果没有在它们的上下文中指定调度器(Dispatcher)或其他任何 ContinuationInterceptor,那么所有标准构建器(如 launch、async 等)所使用的默认 CoroutineDispatcher。
现在我们来看Dispatchers.Main
,为什么它会导致我们拦截失败呢?要探究原因没有直接看源码更加直接有效的。
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
主要看它的类型,它返回的是MainCoroutineDispatcher
,然后再看它是什么
public abstract class MainCoroutineDispatcher : CoroutineDispatcher() {}
发现MainCoroutineDispatcher
继承于CoroutineDispatcher
,主角登场了,但还不够我们继续看CoroutineDispatcher
是什么
CoroutineDispatcher调度器指定执行协程的目标载体,它确定了相关的协程在哪个线程或哪些线程上执行。可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
//将可运行块的执行分派到给定上下文中的另一个线程上
public abstract fun dispatch(context: CoroutineContext, block: Runnable)
//返回一个continuation,它封装了提供的[continuation],拦截了所有的恢复
public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>
//......
}
CoroutineDispatcher
实现了ContinuationInterceptor
,说明CoroutineDispatcher
也具有拦截器的功能。然后再结合CoroutineContext
的性质,就很好解释为什么我们自定义的拦截器没有生效。
原因就是它与我们自定义的拦截器一样都实现了ContinuationInterceptor
接口,一旦使用Dispatchers.Main
就会替换掉我们自定义的拦截器。我们已经知道它具有拦截功能。
协程需要调度的位置就是挂起点的位置,只有当挂起点正在挂起的时候才会进行调度,实现调度需要使用协程的拦截器。
调度的本质就是解决挂起点恢复之后的协程逻辑在哪里运行的问题。调度器也属于协程上下文一类,它继承自拦截器。
再来看CoroutineDispatcher
提供的另外几个方法isDispatchNeeded
与dispatch
。
我们可以大胆猜测,isDispatchNeeded
就是判断是否需要分发,然后dispatch
就是如何进行分发,接下来我们来验证一下。
ContinuationInterceptor
重要的方法就是interceptContinuation
,在CoroutineDispatcher
中直接返回了DispatchedContinuation
对象,它是一个Continuation
类型。那么自然重点就是它的resumeWith
方法。
override fun resumeWith(result: Result<T>) {
val context = continuation.context
val state = result.toState()
if (dispatcher.isDispatchNeeded(context)) {
_state = state
resumeMode = MODE_ATOMIC_DEFAULT
dispatcher.dispatch(context, this)
} else {
executeUnconfined(state, MODE_ATOMIC_DEFAULT) {
withCoroutineContext(this.context, countOrElement) {
continuation.resumeWith(result)
}
}
}
}
这里我们看到了isDispatchNeeded
与dispatch
方法,如果不需要分发自然是直接调用原始的continuation
对象的resumeWith
方法,也就没有什么类似于线程的切换。
那什么时候isDispatcheNeeded
为true
呢?这就要看它的dispatcer
是什么。
由于现在我们是拿Dispatchers.Main
作分析。所以这里我直接告诉你们它的dispatcher
是HandlerContext
override fun createDispatcher(allFactories: List<MainDispatcherFactory>) =
HandlerContext(Looper.getMainLooper().asHandler(async = true), "Main")
internal class HandlerContext private constructor(
private val handler: Handler,
private val name: String?,
private val invokeImmediately: Boolean
) : HandlerDispatcher(), Delay {
/**
* Creates [CoroutineDispatcher] for the given Android [handler].
*
* @param handler a handler.
* @param name an optional name for debugging.
*/
public constructor(
handler: Handler,
name: String? = null
) : this(handler, name, false)
@Volatile
private var _immediate: HandlerContext? = if (invokeImmediately) this else null
override val immediate: HandlerContext = _immediate ?:
HandlerContext(handler, name, true).also { _immediate = it }
override fun isDispatchNeeded(context: CoroutineContext): Boolean {
return !invokeImmediately || Looper.myLooper() != handler.looper
}
override fun dispatch(context: CoroutineContext, block: Runnable) {
handler.post(block)
}
...
}
它继承于HandlerDispatcher
,而HandlerDispatcher
继承于MainCoroutineDispatcher
。
条件都符合,我们直接看isDispatchNeeded
方法返回true
的逻辑。
首先通过invokeImmediately
判断,它代表当前线程是否与自身的线程相同,如何你外部使用者能够保证这一点,就可以直接使用Dispatcher.Main.immediate
来避免进行线程的切换逻辑。当然为了保证外部的判断失败,最后也会通过Looper.myLooper() != handler.looper
来进行校正。对于Dispatchers.Main
这个的handle.looper
自然是主线程的looper
。
如果不能保证则invokeImmediately
为false
,直接进行线程切换。然后进入dispatch
方法,下面是Dispatchers.Main
中dispatch
的处理逻辑。
override fun dispatch(context: CoroutineContext, block: Runnable) {
handler.post(block)
}
这个再熟悉不过了,因为这个时候的handler.post
就是代表向主线程推送消息,此时的block
将会在主线程进行调用。
这样线程的切换就完成。
所以综上来看,CoroutineDispatcher
为协程提供了一个线程切换的统一判断与执行标准。
CoroutineDispatcher实现线程切换的原理如下:
- 首先在协程进行启动的时候通过拦截器的方式进行拦截,对应的方法是
interceptContinuation
- 然后返回一个具有切换线程功能的
Continuation
- 在每次进行
resumeWith
的时候,内部再通过isDispatchNeeded
进行判断当前协程的运行是否需要切换线程。 - 如果需要则调用
dispatch
进行线程的切换,保证协程的正确运行。
如果我要自定义协程线程的切换逻辑,就可以通过继承于CoroutineDispatcher
来实现,将它的核心方法进行自定义即可。
当然,如果你是在Android
中使用协程,那基本上是不需要自定义线程的切换逻辑。因为kotlin
已经为我们提供了日常所需的Dispatchers
。主要有四种分别为:
Dispatchers.Default
: 适合在主线程之外执行占用大量CPU
资源的工作Dispatchers.Main
:Android
主线程Dispatchers.Unconfined
: 它不会切换线程,只是启动一个协程进行挂起,至于恢复之后所在的线程完全由调用它恢复的协程控制。Dispatchers.IO
: 适合在主线程之外执行磁盘或网络I/O
IO仅在 Jvm 上有定义,它基于 Default 调度器背后的线程池,并实现了独立的队列和限制,因此协程调度器从 Default 切换到 IO 并不会触发线程切换。
三、实现原理
Dispatcher调度器实现线程切换的基本实现原理大致为:
- 首先在协程进行启动的时候通过拦截器的方式进行拦截,对应的方法是interceptContinuation
- 然后返回一个具有切换线程功能的Continuation
- 在每次进行resumeWith的时候,内部再通过isDispatchNeeded进行判断当前协程的运行是否需要切换线程。
- 如果需要则调用dispatch进行线程的切换,保证协程的正确运行。如果要自定义协程线程的切换,可以通过继承CoroutineDispatcher来实现。
四、分类
在Kotlin中,协程调度器(Coroutine Dispatcher)是用来决定协程在哪个线程上执行的机制。
4 个 CoroutineDispatcher,它们是做 [任务调度] 也就是切线程的:
Dispatchers.kt
public actual object Dispatchers {
@JvmStatic
public actual val Default: CoroutineDispatcher = DefaultScheduler
@JvmStatic
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
@JvmStatic
public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
@JvmStatic
public val IO: CoroutineDispatcher = DefaultIoScheduler
}
Kotlin的协程库提供了几种不同的调度器,每种都有其特定的用途。以下是Kotlin协程中常见的几种调度器:
- Dispatchers.Default
这是最常用的调度器,用于在多个线程间分配任务。
如果在启动一个协程时没有指定任何的 CoroutineDispatcher,scope.launch 将会使用 Dispatchers.Default 来调度任务,它提供了一个全局的线程池管理任务,启动协程会在它提供的线程池里去运行。——在线程池工作
Dispatchers.Default 主要用于计算密集型任务。
在主线程之外提高对CPU的利用率,例如对list的排序或者JSON的解析。
它是为了平衡CPU密集型和IO密集型任务而设计的。默认调度器会根据可用CPU核心的数量自动选择合适的线程池大小。
- Dispatchers.IO
使Coroutine运行在IO线程,以便执行网络或者I/O操作。——在线程池工作
Dispatchers.IO 主要用于 IO 密集型任务。
专门用于执行I/O操作(如文件读写、网络请求等)。
它与Default调度器不同,它使用了专门为I/O操作优化的线程池。
- Dispatchers.Main
它提供的不是线程池,而是会把任务扔到主线程去执行.——在线程工作
使Coroutine运行中主线程,以便UI操作。Dispatchers.Main 实际上最终也是用 Handler 将我们代码切回到主线程运行。
用于在Android应用的UI线程上执行代码,或者在桌面应用的主线程上执行代码。这是与Android的Looper或桌面应用的UI线程绑定的调度器。
- Dispatchers.Unconfined
这是一个特殊的调度器,不是用来分配到特定线程的,而是在它被启动的地方立即执行。这通常用于测试或者在你知道代码将在正确的上下文中运行时使用(例如,在runBlocking块内)。
像字面意思所说就是 [不限制],它是一个完全不进行线程管理的 ContinuationInterceptor。
挂起函数在该任务调度是不适用的,它不仅在启动协程时不会切线程,而且在挂起函数执行完之后也不会把线程切回去,而是继续在挂起函数所在的那个线程继续执行下面的代码。这种逻辑会让结果非常难以预期,所以实际开发根本不会用到它。
- Custom Dispatcher
可以通过扩展Kotlin的协程库来创建自定义调度器。这可以让你根据特定的需求(如特定的线程池配置)来控制协程的执行。
调度参数两种不同传参模式:
// 复用 CoroutineDispatcher 任务调度,在 CoroutineScope 传参
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {}
scope.launch {}
// scope.launch 传参会覆盖 CoroutineScope 传参
val scope = CoroutineScope(EmptyCoroutineContext)
scope.launch(Dispatchers.IO) {
// 在 Dispatchers.IO 指定的线程池执行
}
两种方式的区别是:
-
如果 scope 要多次 launch 在同一个线程池执行,建议在 CoroutineScope 传参
-
在 scope.launch 传参会覆盖 CoroutineScope 传参,即 launch 传参会高于 CoroutineScope
五、示例
调度器的目的就是切换线程,我们只要提供线程,调度器就应该很方便的创建出来。
创建自定义调度器:
val customDispatcher = newSingleThreadContext("CustomThread")
launch(customDispatcher) {
// 你的代码
}
suspend fun main() {
val myDispatcher= Executors.newSingleThreadExecutor{ r -> Thread(r, "MyThread") }.asCoroutineDispatcher()
GlobalScope.launch(myDispatcher) {
log(1)
}.join()
log(2)
}
由于这个线程池是我们自己创建的,因此我们需要在合适的时候关闭它。
前述我们是通过线程的方式,同理可以通过线程池转为调度器实现。
使用示例
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
fun main() = runBlocking<Unit> {
// 在默认调度器上运行
launch(Dispatchers.Default) {
println("Running in Default: ${Thread.currentThread().name}")
}
// 在IO调度器上运行
launch(Dispatchers.IO) {
println("Running in IO: ${Thread.currentThread().name}")
}
// 在主调度器上运行(仅适用于Android或桌面应用的主线程)
launch(Dispatchers.Main) {
println("Running in Main: ${Thread.currentThread().name}")
}
}
六、注意事项
- 确保不要在非UI线程中更新UI元素,除非你使用了Dispatchers.Main或者在UI线程中启动协程。对于Android,可以使用withContext(Dispatchers.Main)来切换回主线程执行UI更新。
- 对于长时间运行的阻塞操作或密集计算,推荐使用Dispatchers.Default。
- 对于I/O操作,使用Dispatchers.IO可以获得更好的性能。
- 使用自定义调度器时,要确保正确地管理线程的生命周期,以避免内存泄漏或线程安全问题。通常,自定义调度器应该在使用完毕后关闭其关联的线程池。例如:customDispatcher.close()。在Kotlin中,可以使用newFixedThreadPoolContext或newSingleThreadContext等函数来创建自定义调度器。例如:
val customDispatcher = newSingleThreadContext("CustomThread")
try {
launch(customDispatcher) { /* ... */ }
} finally {
customDispatcher.close() // 确保关闭调度器以释放资源
}
推荐文章
https://zhuanlan.zhihu.com/p/301494587