Bootstrap

「最后一次,彻底搞懂kotlin协程」(五) | Dispatcher 与 Kotlin 协程中的线程池

引言

当我发现我不停的看到关于Kotlin协程的文章的时候,我突然意识到:可能现有的文章并没有很好的解决大家的一些问题。在看了一些比较热门的协程文章之后,我确认了这个想法。此时我想起了一个古老的笑话:当一个程序员看到市面上有50种框可用架的时候,决心开发一种框架把这个框架统一起来,于是市面上有了51种框架我最终还是决定:干,因为异步编程实在是一个过于重要的部分。

我总结了现存资料所存在的一些问题:

  1. 官方文档侧重于描述用法,涉及原理部分较少。如果不掌握原理,很难融会贯通,使用时容易踩坑
  2. 部分博客文章基本是把官方文档复述一遍,再辅之少量的示例,增量信息不多
  3. 不同的博客文章之间内容质量参差不齐,理解角度和描述方式各不相同,部分未经验证的概念反而混淆了认知,导致更加难以理解
  4. 部分博客文章涉及大量源码相关内容,但描述线索不太容易理解,缺乏循序渐进的讲述和一些关键概念的铺垫和澄清

地心说与日心说.gif

而为什么 coroutine 如此难以描述清楚呢?我总结了几个原因:

  1. 协程的结构化并发写法(异步变同步的写法)很爽,但与之前的经验相比会过于颠覆,难以理解
  2. 协程引入了不少之前少见的概念,CoroutineScope,CoroutineContext… 新概念增加了理解的难度
  3. 协程引入了一些魔法,比如 suspend,不仅是一个关键字,更在编译时加了料,而这个料恰好又是搞懂协程的关键
  4. 协程的恢复也是非常核心的概念,是协程之所以为协程而不单单只是另一个线程框架的关键,而其很容易被一笔带过
  5. 因为协程的“新”概念较多,技术实现也较为隐蔽,所以其主线也轻易的被掩埋在了魔法之中

那么在意识到了理解协程的一些难点之后,本文又将如何努力化解这些难题呢?我打算尝试以下一些方法:

  1. 从基础的线程开始,澄清一些易错而又对理解协程至关重要的概念,尽量不引入对于理解协程核心无关的细节
  2. 循序渐进,以异步编程的发展脉络为主线,梳理引入协程后解决了哪些问题,也破除协程的高效率迷信
  3. 物理学家费曼有句话:“What I cannot create, I do not understand”,我会通过一些简陋的模拟实现,来降低协程陡峭的学习曲线
  4. 介绍一些我自己的独特理解,并引入一些日常场景来加强对于协程理解
  5. 加入一些练习。如看不练理解会很浅,浅显的理解会随着时间被你的大脑垃圾回收掉,陷入重复学习的陷阱(这也是本系列标题夸口最后一次的原因之一)

回顾

在上一篇异步变同步中笔者以异步变同步为线索讲述了 suspend,Coroutine 背后挂起与恢复的机制,并澄清了对于 suspend 概念的误解。这一篇我们就来看看与异步编程紧密相关的 Dispatcher 以及 kotlin 中的 Handler,线程池,看看 Handler 和线程池怎么在 Kotlin 协程中发挥作用。在线程池篇中我们已经涉及了部分内容,没看过的读者可以先回过头去看看。

启示

我们以一个看似无关的问题开始:Android 中的网络请求框架经历了怎样的演变,异步在其中扮演了什么样的角色?看文章的小伙伴可以先停下来思考一下,搞清楚这个问题有助于后面内容的理解。

下面我以异步为线索,梳理一下网络请求方式大致的几个发展阶段:

  1. 用 Java 的 HttpURLClient 或者 Apache 的 HttpClient 做 client,搭配自己控制的线程池
  2. Android API 3 推出了 AsyncTask,内部有一个工作线程,搭配 1 中的 client
  3. Google 推出 volley,使用 1 中的 client,还自带了内部的线程池控制,摆脱了 AsyncTask 不灵活的API 与串行的限制
  4. Square 推出 OkHttp,内部有自己的 http client,以及自有的线程池,可以看作是 volley 的加强版
  5. Square 推出 Retrofit,简化了网络请求的操作,OkHttp + Retrofit 基本统一了市场
  6. Rxjava 推出后,Retrofit 通过 Rxjava-Adapter 支持,形成 OKHttp + Retrofit + Rxjava 的组合
  7. 在 Kotlin Coroutine 推出后,Retrofit 在 2.6 版本内部提供了支持,形成 OKHttp + Retrofit + Coroutine 的组合

从 1 -> 2 可以看出,Android 使用 AsyncTask,替代了线程池,隐藏了任务在不同线程间的切换,其内部本身是一个任务队列 + 单线程。2 -> 3 可以看作是对不够灵活且不能并发的 AsyncTask 的升级,使用的依然是相同的 Client。我们从 1- 3 可以看出,httpClient 本身没有发生变化,变化的是如何做异步的逻辑

3 -> 4 是一个大版本的升级,OkHttp 中推出了自己的 Http Client (早期版本可以使用其他的 http cleint 实现),与 Volley 类似,内部同样维护了自己的线程池。4 -> 5 不仅简化了网络请求操作,并且通过 CallAdaptor 机制,扩展了 OkHttp 适配其他异步编程框架的能力

5 -> 6 就是利用了这个能力,把 Call 适配成了 Rxjava 的可观察对象,充分利用了 Rxjava 的异步能力。6 -> 7 类似,只是把适配的 Rxjava 的对象换成了 suspend 函数,或者是 Flow 对象,利用了 Kotlin Coroutine 的非阻塞式的异步能力,变化的依然是如何做异步的逻辑

根据从上面的趋势,我们可以说:网络请求的发展有两条主线,一条是与发起 Http 请求相关的以 HttpClient 为核心的进化,另外一条是异步能力的进化,从自己使用线程池,到把线程池封装到 Http 框架内部,后面随着以 Rxjava 和 Kotlin Coroutine 为代表的异步框架 的成熟,网络请求框架中的线程池又逐渐被剥离出来,网络请求和异步能力又被解耦了,Retrofit 适应了这个趋势(这就叫扩展性),所以如今依然是网络请求的主流。而不管是 Rxjava 还是 Kotlin 协程,其内部提供异步能力的核心,都是线程池。我们下面就来看看 Kotlin 协程内部的线程线程池,以及与之密切相关的 Dispatcher。

Dispatchers

Dispatchers.IO

我们先从 Dispatchers.IO 开始,先来看看相关源码,对其有一个基本认知

kotlin
复制代码
// Dispachers
public val IO: CoroutineDispatcher = DefaultIoScheduler

// DefaultIoScheduler
internal object DefaultIoScheduler : ExecutorCoroutineDispatcher(), Executor {
  	// 1. dispatcher
    private val default: CoroutineDispatcher = UnlimitedIoScheduler.limitedParallelism(
       systemProp(
           IO_PARALLELISM_PROPERTY_NAME,
           64.coerceAtLeast(AVAILABLE_PROCESSORS) // 1.1. 64
       )
    ) 
  
  	// 2 Executor.execute
    override fun execute(command: java.lang.Runnable) = dispatch(EmptyCoroutineContext, command)

  	// 3 Dispatcher.dispatch
    override fun dispatch(context: CoroutineContext, block: Runnable) {
      default.dispatch(context, block)
    }
}

Dispatchers.IO 内部是一个 DefaultIoScheduler 对象,对象实现了 ExecutorCoroutineDispatcher 和 Executor 接口,1.1 中的64代表的是 Dispatcher 貌似是内部最大的线程数,2 是复写的 Executor 接口的 execute 方法,3 是复写的 CoroutineDispatcher 的 dispatch 方法,可以看到 2 中的 execute 方法其实是调用了 3 中的 dispatch 方法。

下面我们就来实际使用一下:

kotlin
复制代码
// Coroutine1.kt
fun main() = runBlocking {
    val threads = Collections.synchronizedSet<String>(mutableSetOf())

    val time = measureTime {
        List(1000) {
            async(Dispatchers.IO) {
                threads.add(Thread.currentThread().name)
                Thread.sleep(1000)
            }
        }.awaitAll()
    }

    println("time: $time threads.size: ${threads.size}")
}

// log
time: 16.066230875s threads.size: 64

这个例子的 log 印证了我们上面的猜想,确实是最大 64 个线程,执行时间也接近理想值。所以我们可以认为 Dispatchers.IO 可以维护一个至多 64 个线程的线程池,这个 Dispatchers.IO 甚至可以转化为 Executor 直接使用,Executor 也可以通过转化为 Dispatcher使用,不清楚的小伙伴可以复习一下线程池篇

下面我们稍微深入源码一下看看这个 64 是如何实现的:

kotlin
复制代码
// 1. DefaultIoScheduler
private val default = UnlimitedIoScheduler.limitedParallelism(
    systemProp(
        IO_PARALLELISM_PROPERTY_NAME,
        64.coerceAtLeast(AVAILABLE_PROCESSORS)
    )
)

// 2. UnlimitedIoScheduler
override fun limitedParallelism(parallelism: Int): CoroutineDispatcher {
    parallelism.checkParallelism()
    if (parallelism >= MAX_POOL_SIZE) return this
    return super.limitedParallelism(parallelism) // *
}

// 3. CoroutineDispatcher
public open fun limitedParallelism(parallelism: Int): CoroutineDispatcher {
    parallelism.checkParallelism()
    return LimitedDispatcher(this, parallelism)
}

// 4. LimitedDispatcher
private inline fun dispatchInternal(block: Runnable, startWorker: (Worker) -> Unit) {
    // 4.1 Add task to queue so running workers will be able to see that
    queue.addLast(block)
  
  	// 4.2 Add Worker
    if (runningWorkers.value >= parallelism) return
    if (!tryAllocateWorker()) return
    val task = obtainTaskOrDeallocateWorker() ?: return
    startWorker(Worker(task))
}

Dispatcher.IO 的源码最终深入到了 LimitedDispatcher,LimitedDispatcher 的 dispatchInternal 是 dispatch 调用的方法,首先在 4.1 把任务加到 dispatcher 的队列中,然后 4.2 检查 worker(个人觉得这个命名不好,这里的worker不是线程,类似于不会阻塞的 EventLooper,运行期间会占用一个线程) 的数量是否达到传入的上限,没有就会在内部去检查是否任务已经堆积,如果已经堆积了就增加一个 worker 并启动,真正工作的线程也是一个 Worker,在 CoroutineScheduler 里面,下面我们会讲到。

Dispatchers.Default

同样先来看看相关源码

kotlin
复制代码
// 1. Dispatchers
public actual val Default: CoroutineDispatcher = DefaultScheduler

// 2. DefaultScheduler
internal object DefaultScheduler : SchedulerCoroutineDispatcher(
    CORE_POOL_SIZE, MAX_POOL_SIZE,
    IDLE_WORKER_KEEP_ALIVE_NS, DEFAULT_SCHEDULER_NAME
) {

    override fun limitedParallelism(parallelism: Int): CoroutineDispatcher {
        parallelism.checkParallelism()
        if (parallelism >= CORE_POOL_SIZE) return this
        return super.limitedParallelism(parallelism)
    }
}

internal val CORE_POOL_SIZE = systemProp(
    "kotlinx.coroutines.scheduler.core.pool.size",
    AVAILABLE_PROCESSORS.coerceAtLeast(2),
    minValue = CoroutineScheduler.MIN_SUPPORTED_POOL_SIZE
)

// 3. SchedulerCoroutineDispatcher
internal open class SchedulerCoroutineDispatcher(
    private val corePoolSize: Int = CORE_POOL_SIZE,
    private val maxPoolSize: Int = MAX_POOL_SIZE,
    private val idleWorkerKeepAliveNs: Long = IDLE_WORKER_KEEP_ALIVE_NS,
    private val schedulerName: String = "CoroutineScheduler",
) : ExecutorCoroutineDispatcher() {

  	// 3.1
    private var coroutineScheduler = CoroutineScheduler(corePoolSize, maxPoolSize, idleWorkerKeepAliveNs, schedulerName)

  	// 3.2
    override fun dispatch(context: CoroutineContext, block: Runnable): Unit = coroutineScheduler.dispatch(block)
}

// 4. CoroutineScheduler
// Coroutine scheduler (pool of shared threads) which primary target is to distribute dispatched coroutines
over worker threads, including both CPU-intensive and blocking tasks, in the most efficient manner.
internal class CoroutineScheduler(
    @JvmField val corePoolSize: Int,
    @JvmField val maxPoolSize: Int,
    @JvmField val idleWorkerKeepAliveNs: Long = IDLE_WORKER_KEEP_ALIVE_NS,
    @JvmField val schedulerName: String = DEFAULT_SCHEDULER_NAME
) : Executor {
  	// 4.1 
    val globalCpuQueue = GlobalQueue()
  	// 4.2
    val globalBlockingQueue = GlobalQueue()
}

// 5. CoroutineScheduler.Worker
internal inner class Worker private constructor() : Thread() {
  val localQueue: WorkQueue = WorkQueue()
}

Dispatchers.Default 内部是 DefaultScheduler 对象,其继承了 SchedulerCoroutineDispatcher,从其传入的参数看,SchedulerCoroutineDispatcher 已经把线程池写在了脸上。3.1 其内部又是通过 CoroutineScheduler 实现,从 CoroutineScheduler 的注释来看,CoroutineScheduler 是 Kotlin Coroutine 中的共享线程池,其内部的线程就是 CoroutineScheduler.Worker。

我们同样以一个例子来看看这里面有多少线程:

kotlin
复制代码
// Coroutine2.kt
fun main() = runBlocking {
    val threads = Collections.synchronizedSet<String>(mutableSetOf())

    val time = measureTime {
        List(100) {
            async(Dispatchers.Default) {
                threads.add(Thread.currentThread().name)
              	// * Possibly blocking call in non-blocking context could lead to thread starvation 
                Thread.sleep(1000)
            }
        }.awaitAll()
    }

    println("time: $time threads.size: ${threads.size}")
}

// log
time: 9.042411958s threads.size: 12

12 个线程,9秒运行完毕,时间符合预期。可是 12 是是怎么来的呢,我们看看上面 DefaultScheduler 传入的 CORE_POOL_SIZE

kotlin
复制代码
// 1
internal val CORE_POOL_SIZE = systemProp(
    "kotlinx.coroutines.scheduler.core.pool.size",
    AVAILABLE_PROCESSORS.coerceAtLeast(2),
    minValue = CoroutineScheduler.MIN_SUPPORTED_POOL_SIZE
)

// 2
internal val AVAILABLE_PROCESSORS = Runtime.getRuntime().availableProcessors()

CORE_POOL_SIZE,读取了一个属性,此属性无值便读取 AVAILABLE_PROCESSORS,字面意思,可用的处理器核心数量,这个 12 是我目前运行程序的 Mac 的 CPU 核心数量,不同的运行环境可能会得到不同的数值。

细心的读者可能发现了上面 Coroutine2.kt 例子中带 * 的注释,翻译过来是可能会在非阻塞的上下文中导致线程饥饿。这有点奇怪,那 Coroutine1.kt 中会有同样警告吗?答案是:没有。那编译器的意思是:Dispatchers.Default 是非阻塞的,Dispatchers.IO 就是可阻塞的咯?答案是:没错。我们先逆向在源码中来找寻一下答案:

kotlin
复制代码
// 1. CoroutineScheduler
private fun addToGlobalQueue(task: Task): Boolean {
    return if (task.isBlocking) {
        globalBlockingQueue.addLast(task)
    } else {
        globalCpuQueue.addLast(task)
    }
}

// 2. CoroutineScheduler
fun dispatch(block: Runnable, taskContext: TaskContext = NonBlockingContext, tailDispatch: Boolean = false) {
  ···
  val task = createTask(block, taskContext)
  addToGlobalQueue(task)
  ···
}

// 3. SchedulerCoroutineDispatcher 即 Dispatchers.Default
override fun dispatch(context: CoroutineContext, block: Runnable): Unit = coroutineScheduler.dispatch(block)

// 4. UnlimitedIoScheduler 即 Dispatchers.IO
override fun dispatch(context: CoroutineContext, block: Runnable) {
    DefaultScheduler.dispatchWithContext(block, BlockingContext, false)
}

在 1 中,如果 task 的 isBlocking 为 true,就加到 globalBlockingQueue 中,反之加到 globalCpuQueue 中。2中接受来 TaskContext 参数,默认为 NonBlockingContext,这个参数决定了 task 是否为 blocking。 3 代表 Dispatchers.Default,传入的参数中不包含 taskContext,即 NonBlockingContext。4 代表 Dispatchers.IO,在参数中传入了 BlockingContext。这是源码给出的答案,可为什么会这样呢?

答案就在两个变量名中,globalCpuQueueglobalBlockingQueue。我们知道 cpu 运行速度很快,通常不会成为 IO 操作的瓶颈,反之 IO 操作常常可能成为 cpu 操作的瓶颈,所以 kotlin 协程把任务分为两种,一种是 cpu 密集型的,不会有 IO 等待,执行这个任务的线程不会导致作为 cpu 分配最小单位的线程的饥饿(线程的饥饿导致内存的浪费,不做事的线程浪费了大量的内存)。而另外一种是可能会阻塞执行线程的,会被放到 globalBlockingQueue 中,IO 操作符合这个特点。这里的阻塞是为了协调两种不同操作类型可能导致的资源浪费,和主线程的阻塞并不相同,主线程的阻塞是指一个任务执行时间过长,后面更新界面的任务被延迟执行导致界面卡顿,是有时效性的任务被延迟了,并没有线程被阻塞,也不涉及到资源的浪费。

共享线程池

我们来复习一下线程池篇中的一个关于执行线程的例子:

kotlin
复制代码
// 线程池篇 coroutine4.kt
fun main() {
    val coroutineScope = CoroutineScope(Dispatchers.Default)

    coroutineScope.launch {
        // 1. context[ContinuationInterceptor] = Dispatchers.Default
        printlnWithThread("work1")
        val work2 = withContext(Dispatchers.IO) {
            // 2. context[ContinuationInterceptor] = Dispatchers.IO
            printlnWithThread("do work2 ...")
            "work2"
        }
        // 3. // context[ContinuationInterceptor] = Dispatchers.Default
        printlnWithThread(work2)
    }

    Thread.sleep(100)
}

// log
DefaultDispatcher-worker-1: work1
DefaultDispatcher-worker-1: do work2 ...
DefaultDispatcher-worker-3: work2

可以看到 work1 和 work2 是由同一个线程执行的,当时我说的 work1 和 work2 确发生了流转线程的操作,不过因为 DefaultDispatcher-worker-1 线程在执行完 work1 之后已经空闲了,并且 Dispatchers.Default 和 Dispatchers.IO 的线程池是互相复用的,所以发生了 work1 和 work2 由同一个线程执行的情况。

不过,这种复用是怎么发生的呢?其实答案在上面的源码中已经出现过了,我们再看一下:

kotlin
复制代码
// UnlimitedIoScheduler 即 Dispatchers.IO
override fun dispatch(context: CoroutineContext, block: Runnable) {
    DefaultScheduler.dispatchWithContext(block, BlockingContext, false)
}

// DefaultScheduler 即 Dispatchers.Default

Dispatchers.IO 内部在 dispatch 时直接调用了 Dispatchers.Default 做分发,不过在调用时传入了 BlockingContext 以此和 Dispatchers.Default 做区分,线程本身并没有 BlockingContext 和 NonBlockingContext 之分,只是在使用不同的 Dispatchers 时用为 Task 做了不同的标记。一个线程即可能做 NonBlockingTask 也可能做 BlockingTask,通过线程池之间的相互复用,协程内部又一次提高了资源的利用率。

我们前面提到 Dispatchers.IO 内部的线程为 64,在笔者的 Mac 上,Dispatchers.Default 的线程是 12,既然两者的线程发生了复用,那两者同时使用的话总的线程数应该是多少呢,64,76?我们用一个例子来测试看看:

kotlin
复制代码
// Coroutine3.kt
fun main() = runBlocking {
    val threads = Collections.synchronizedSet<String>(mutableSetOf())

    val time = measureTime {
        val deferred1 = async {
            List(100) {
                async(Dispatchers.Default) {
                    threads.add(Thread.currentThread().name)
                    Thread.sleep(1000)
                }
            }.awaitAll()
        }

        val deferred2 = async {
            List(1000) {
                async(Dispatchers.IO) {
                    threads.add(Thread.currentThread().name)
                    Thread.sleep(1000)
                }
            }.awaitAll()
        }

        deferred1.await()
        deferred2.await()
    }
    println("time: $time threads.size: ${threads.size}")
}

// log
time: 16.067898167s threads.size: 76

总的执行时间是两个任务中长的那个,即 16s,线程数也是 76,即 64 + 12,两者之和。这说明:两者只是复用线程池,但并不会影响各自的线程数量,一般情况线程池的数量不会达到饱和,所以两个线程池间相互复用线程还是能带来实质的受益。

Dispatchers.Main

我们还是先看源码:

kotlin
复制代码
// 1. Dispatchers.Main
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher

// 2. MainDispatcherLoader
internal object MainDispatcherLoader {

    val dispatcher: MainCoroutineDispatcher = loadMainDispatcher()

    private fun loadMainDispatcher(): MainCoroutineDispatcher {
        return try {
            val factories = if (FAST_SERVICE_LOADER_ENABLED) {
              	// 2.1 fast
                FastServiceLoader.loadMainDispatcherFactory()
            } else {
              	// 2.2 slow
                ServiceLoader.load(
                        MainDispatcherFactory::class.java,
                        MainDispatcherFactory::class.java.classLoader
                ).iterator().asSequence().toList()
            }
          
          	// 2.3 create MainCoroutineDispatcher
            factories.maxByOrNull { it.loadPriority }?.tryCreateDispatcher(factories)
                ?: createMissingDispatcher()
        }
    }
}

// 3. fast
internal fun loadMainDispatcherFactory(): List<MainDispatcherFactory> {
  	// 3.1 slow
    val clz = MainDispatcherFactory::class.java
    if (!ANDROID_DETECTED) {
        return load(clz, clz.classLoader)
    }
  
  	// 3.2 create factory
    return try {
        val result = ArrayList<MainDispatcherFactory>(2)
      	// 3.3 formal
        createInstanceOf(clz, "kotlinx.coroutines.android.AndroidDispatcherFactory")?.apply { result.add(this) }
      	// 3.4 test
        createInstanceOf(clz, "kotlinx.coroutines.test.internal.TestMainDispatcherFactory")?.apply { result.add(this) }
        result
    }
}

Dispatchers.Main 内部通过 MainDispatcherLoader.loadMainDispatcher() 方法来加载。这看起来跟前面的 Dispatchers.Default 和 Dispatchers.IO 不太一样,我们继续往下看,在 loadMainDispatcher() 方法中,整体是先加载一个 factory,再用 factory.tryCreateDispatcher(factories) 创建 MainCoroutineDispatcher,加载分为 fast(2.1) 和 slow(2.2) 两种方式,这里我们关注 fast(3)。fast 内部判断了是否可以检测到 Android Platform,如果不是 Android Platform,则又走回 slow(3.1) 遍历的方式,fast load 在于 3.3 和 3.4,3.4 是用于测试的,所以我们关注的是 3.3: kotlinx.coroutines.android.AndroidDispatcherFactory

如果没有直接或者间接加入org.jetbrains.kotlinx:kotlinx-coroutines-android依赖的话这个类是找不到的,在加入依赖后,我们来看看关键的代码:

kotlin
复制代码
// 1. AndroidDispatcherFactory
internal class AndroidDispatcherFactory : MainDispatcherFactory {

    override fun createDispatcher(allFactories: List<MainDispatcherFactory>): MainCoroutineDispatcher {
      	// * mainLooper
        val mainLooper = Looper.getMainLooper() ?: throw IllegalStateException("The main looper is not available")
        return HandlerContext(mainLooper.asHandler(async = true))
    }
}

// 2. HandlerContext
internal class HandlerContext private constructor(
    private val handler: Handler,
    private val name: String?,
    private val invokeImmediately: Boolean
) : HandlerDispatcher(), Delay {
		
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        if (!handler.post(block)) {
            cancelOnRejection(context, block)
        }
    }
  
}

在 part1 中,我们看到了 mainLooper 的身影,并且用 mainLooper 构造了一个 HandlerContext,这就是我们的 MainCoroutineDispatcher,我们进入 HandlerContext 看看细节,其继承了 HandlerDispatcher,HandlerDispatcher 实现了 MainCoroutineDispatcher。再来看看关键的 dispatch 方法,不出所料,用 handler.post(block) 方法实现,这样我们就解析完了从加载到 dispatch 的解析。

Tips

MainCoroutineDispatcher 有一个 immediate 属性,我们通常应该使用这个属性,其作用为:如果我们本来已经在主线程上,这个任务会被立即执行,这也可以帮助我们避免再一次的 dispatch。如果不使用,dispatch 会把任务正常放到主线程的任务队列后面,这意味着执行也会被推后,这样我们在主线程上的操作:比如更新界面,就会被推迟,其使用方式像下面这样:

kotlin
复制代码
suspend fun showFeed(feed: Feed) =
    withContext(Dispatchers.Main.immediate) {
        // ...
    }

总结

隐藏在网络请求库发展背后的主线之一是异步框架的发展,而异步的基础是线程和线程池,Kotlin 协程也不例外,本篇我们介绍了 Dispatchers.IO 和 Dispatchers.Default 背后的线程池的实现,讲解了两者实质性的不同(可阻塞与不可阻塞),以及 Dispatchers.Main 的加载与实现方式.

讲到这里,我们已经把 Kotlin 协程里面最重要的一些基本概念都涵盖到了。下一篇,我们就用这些知识来搞个大事,大概是全网唯一:手撸一个 Kotlin 协程 Demo,把整个知识体系串起来,下一篇见。

示例源码github.com/chdhy/kotli…
练习:查看 Dispatchers 中的代码,包括 Dispatchers.Unconfined

点赞👍文章,关注❤️ 笔者,获取后续文章更新

  1. 「最后一次,彻底搞懂kotlin协程」(一) | 先回到线程
  2. 「最后一次,彻底搞懂kotlin协程」(二) | 线程池,Handler,Coroutine
  3. 「最后一次,彻底搞懂kotlin协程」(三) | CoroutineScope,CoroutineContext,Job: 结构化并发
  4. 「最后一次,彻底搞懂kotlin协程」(四) | suspend挂起,EventLoop恢复:异步变同步的秘密
  5. 「最后一次,彻底搞懂kotlin协程」(五) | Dispatcher 与 Kotlin 协程中的线程池

作者:hunterXYZ
链接:https://juejin.cn/post/7373505141490794507
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

;