引言
当我发现我不停的看到关于Kotlin协程的文章的时候,我突然意识到:可能现有的文章并没有很好的解决大家的一些问题。在看了一些比较热门的协程文章之后,我确认了这个想法。此时我想起了一个古老的笑话:当一个程序员看到市面上有50种框可用架的时候,决心开发一种框架把这个框架统一起来,于是市面上有了51种框架
我最终还是决定:干,因为异步编程实在是一个过于重要的部分。
我总结了现存资料所存在的一些问题:
- 官方文档侧重于描述用法,涉及原理部分较少。如果不掌握原理,很难融会贯通,使用时容易
踩坑
- 部分博客文章基本是把官方文档复述一遍,再辅之少量的示例,
增量信息
不多 - 不同的博客文章之间内容质量参差不齐,理解角度和描述方式各不相同,部分
未经验证
的概念反而混淆了认知,导致更加难以理解 - 部分博客文章涉及大量源码相关内容,但描述
线索
不太容易理解,缺乏循序渐进的讲述和一些关键概念的铺垫和澄清
而为什么 coroutine 如此难以描述清楚呢?我总结了几个原因:
- 协程的结构化并发写法(异步变同步的写法)很爽,但与之前的经验相比会过于颠覆,难以理解
- 协程引入了不少之前少见的概念,CoroutineScope,CoroutineContext…
新概念
增加了理解的难度 - 协程引入了一些
魔法
,比如 suspend,不仅是一个关键字,更在编译时加了料,而这个料恰好又是搞懂协程的关键 - 协程的恢复也是非常核心的概念,是协程之所以为协程而不单单只是另一个线程框架的关键,而其很容易被一笔带过
- 因为协程的“新”概念较多,技术实现也较为
隐蔽
,所以其主线也轻易的被掩埋在了魔法之中
那么在意识到了理解协程的一些难点之后,本文又将如何努力化解这些难题呢?我打算尝试以下一些方法:
- 从基础的线程开始,
澄清
一些易错而又对理解协程至关重要的概念,尽量不引入对于理解协程核心无关的细节 循序渐进
,以异步编程的发展脉络为主线
,梳理引入协程后解决了哪些问题,也破除协程的高效率迷信- 物理学家费曼有句话:“What I cannot create, I do not understand”,我会通过一些简陋的
模拟实现
,来降低协程陡峭的学习曲线
- 介绍一些我自己的
独特理解
,并引入一些日常场景来加强对于协程理解 - 加入一些
练习
。如看不练理解会很浅,浅显的理解会随着时间被你的大脑垃圾回收掉,陷入重复学习的陷阱(这也是本系列标题夸口最后一次的原因之一)
回顾
在上一篇异步变同步中笔者以异步变同步为线索
讲述了 suspend,Coroutine 背后挂起与恢复的机制,并澄清了对于 suspend 概念的误解。这一篇我们就来看看与异步编程紧密相关的 Dispatcher 以及 kotlin 中的 Handler,线程池,看看 Handler 和线程池怎么在 Kotlin 协程中发挥作用。在线程池篇中我们已经涉及了部分内容,没看过的读者可以先回过头去看看。
启示
我们以一个看似无关的问题开始:Android 中的网络请求框架经历了怎样的演变,异步在其中扮演了什么样的角色?看文章的小伙伴可以先停下来思考一下,搞清楚这个问题有助于后面内容的理解。
下面我以异步为线索,梳理一下网络请求方式大致的几个发展阶段:
- 用 Java 的 HttpURLClient 或者 Apache 的 HttpClient 做 client,搭配自己控制的线程池
- Android API 3 推出了 AsyncTask,内部有一个工作线程,搭配 1 中的 client
- Google 推出 volley,使用 1 中的 client,还自带了内部的线程池控制,摆脱了 AsyncTask 不灵活的API 与串行的限制
- Square 推出 OkHttp,内部有自己的 http client,以及自有的线程池,可以看作是 volley 的加强版
- Square 推出 Retrofit,简化了网络请求的操作,OkHttp + Retrofit 基本统一了市场
- Rxjava 推出后,Retrofit 通过 Rxjava-Adapter 支持,形成 OKHttp + Retrofit + Rxjava 的组合
- 在 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。这是源码给出的答案,可为什么会这样呢?
答案就在两个变量名中,globalCpuQueue 和 globalBlockingQueue。我们知道 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
点赞👍文章,关注❤️ 笔者,获取后续文章更新
- 「最后一次,彻底搞懂kotlin协程」(一) | 先回到线程
- 「最后一次,彻底搞懂kotlin协程」(二) | 线程池,Handler,Coroutine
- 「最后一次,彻底搞懂kotlin协程」(三) | CoroutineScope,CoroutineContext,Job: 结构化并发
- 「最后一次,彻底搞懂kotlin协程」(四) | suspend挂起,EventLoop恢复:异步变同步的秘密
- 「最后一次,彻底搞懂kotlin协程」(五) | Dispatcher 与 Kotlin 协程中的线程池
作者:hunterXYZ
链接:https://juejin.cn/post/7373505141490794507
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。