Kotlin协程详解——调度器Dispatcher

目录

一、调度器应用原因

1.简化异步编程

2.优化线程管理

3.提高应用性能

4.支持多种应用场景

二、源码分析

三、实现原理

四、分类

五、示例

六、注意事项


一、调度器应用原因

调度器应用的目的是切换线程,切线程是不希望要切线程的代码挡住我当前的线程,所以需要开一个并行的线程执行这段代码。调度器即确认相关协程在哪个线程上执行,调度的本质是解决挂起恢复后协程逻辑在哪里运行的问题,其继承自拦截器。

并行线程叫做子线程或后台线程

而不希望被挡住的线程在 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提供的另外几个方法isDispatchNeededdispatch

我们可以大胆猜测,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)
            }
        }
    }
}

这里我们看到了isDispatchNeededdispatch方法,如果不需要分发自然是直接调用原始的continuation对象的resumeWith方法,也就没有什么类似于线程的切换。

那什么时候isDispatcheNeededtrue呢?这就要看它的dispatcer是什么。

由于现在我们是拿Dispatchers.Main作分析。所以这里我直接告诉你们它的dispatcherHandlerContext

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

如果不能保证则invokeImmediatelyfalse,直接进行线程切换。然后进入dispatch方法,下面是Dispatchers.Maindispatch的处理逻辑。

override fun dispatch(context: CoroutineContext, block: Runnable) {
    handler.post(block)
}

这个再熟悉不过了,因为这个时候的handler.post就是代表向主线程推送消息,此时的block将会在主线程进行调用。

这样线程的切换就完成。

所以综上来看,CoroutineDispatcher为协程提供了一个线程切换的统一判断与执行标准。

CoroutineDispatcher实现线程切换的原理如下:

  • 首先在协程进行启动的时候通过拦截器的方式进行拦截,对应的方法是interceptContinuation
  • 然后返回一个具有切换线程功能的Continuation
  • 在每次进行resumeWith的时候,内部再通过isDispatchNeeded进行判断当前协程的运行是否需要切换线程。
  • 如果需要则调用dispatch进行线程的切换,保证协程的正确运行。

如果我要自定义协程线程的切换逻辑,就可以通过继承于CoroutineDispatcher来实现,将它的核心方法进行自定义即可。

当然,如果你是在Android中使用协程,那基本上是不需要自定义线程的切换逻辑。因为kotlin已经为我们提供了日常所需的Dispatchers。主要有四种分别为:

  1. Dispatchers.Default: 适合在主线程之外执行占用大量CPU资源的工作
  2. Dispatchers.MainAndroid主线程
  3. Dispatchers.Unconfined: 它不会切换线程,只是启动一个协程进行挂起,至于恢复之后所在的线程完全由调用它恢复的协程控制。
  4. Dispatchers.IO: 适合在主线程之外执行磁盘或网络I/O

IO仅在 Jvm 上有定义,它基于 Default 调度器背后的线程池,并实现了独立的队列和限制,因此协程调度器从 Default 切换到 IO 并不会触发线程切换。

三、实现原理

Dispatcher调度器实现线程切换的基本实现原理大致为:

  1. 首先在协程进行启动的时候通过拦截器的方式进行拦截,对应的方法是interceptContinuation
  2. 然后返回一个具有切换线程功能的Continuation
  3. 在每次进行resumeWith的时候,内部再通过isDispatchNeeded进行判断当前协程的运行是否需要切换线程。
  4. 如果需要则调用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

https://zhuanlan.zhihu.com/p/552225674

1.1-协程基础与关键知识:切线程 launch_newfixedthreadpoolcontext-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

闲暇部落

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值