Kotlin协程到底是怎么切换线程的?你是否知晓?

article/2025/11/6 8:32:01

好文推荐
作者:RicardoMJiang

前言

之前对协程做了一个简单的介绍,回答了协程到底是什么的问题,感兴趣的同学可以了解下:【带着问题学】协程到底是什么?
通过上文,我们了解了以下内容
1.kotlin协程本质上对线程池的封装
2.kotlin协程可以用同步方式写异步代码,自动实现对线程切换的管理

这就引出了本文的主要内容,kotlin协程到底是怎么切换线程的?
具体内容如下:

1. 前置知识

1.1 CoroutineScope到底是什么?

CoroutineScope即协程运行的作用域,它的源码很简单

public interface CoroutineScope {public val coroutineContext: CoroutineContext
}

可以看出CoroutineScope的代码很简单,主要作用是提供CoroutineContext,协程运行的上下文
我们常见的实现有GlobalScope,LifecycleScope,ViewModelScope

1.2 GlobalScopeViewModelScope有什么区别?

public object GlobalScope : CoroutineScope {/*** 返回 [EmptyCoroutineContext].*/override val coroutineContext: CoroutineContextget() = EmptyCoroutineContext
}public val ViewModel.viewModelScope: CoroutineScopeget() {val scope: CoroutineScope? = this.getTag(JOB_KEY)if (scope != null) {return scope}return setTagIfAbsent(JOB_KEY,CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))}

两者的代码都挺简单,从上面可以看出
1.GlobalScope返回的为CoroutineContext的空实现
2.ViewModelScope则往CoroutineContext中添加了JobDispatcher

我们先来看一段简单的代码

	fun testOne(){GlobalScope.launch {print("1:" + Thread.currentThread().name)delay(1000)print("2:" + Thread.currentThread().name)}}//打印结果为:DefaultDispatcher-worker-1fun testTwo(){viewModelScope.launch {print("1:" + Thread.currentThread().name)delay(1000)print("2:" + Thread.currentThread().name)}}//打印结果为: main

上面两种Scope启动协程后,打印当前线程名是不同的,一个是线程池中的一个线程,一个则是主线程
这是因为ViewModelScopeCoroutineContext中添加了Dispatchers.Main.immediate的原因

我们可以得出结论:协程就是通过Dispatchers调度器来控制线程切换的

1.3 什么是调度器?

从使用上来讲,调度器就是我们使用的Dispatchers.Main,Dispatchers.DefaultDispatcher.IO
从作用上来讲,调度器的作用是控制协程运行的线程
从结构上来讲,Dispatchers的父类是ContinuationInterceptor,然后再继承于CoroutineContext
它们的类结构关系如下:

这也是为什么Dispatchers能加入到CoroutineContext中的原因,并且支持+操作符来完成增加

1.4 什么是拦截器

从命名上很容易看出,ContinuationInterceptor即协程拦截器,先看一下接口

interface ContinuationInterceptor : CoroutineContext.Element {// ContinuationInterceptor 在 CoroutineContext 中的 Keycompanion object Key : CoroutineContext.Key<ContinuationInterceptor>/*** 拦截 continuation*/fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>//...
}

从上面可以提炼出两个信息
1.拦截器的Key是单例的,因此当你添加多个拦截器时,生效的只会有一个
2.我们都知道,Continuation在调用其Continuation#resumeWith()方法,会执行其suspend修饰的函数的代码块,如果我们提前拦截到,是不是可以做点其他事情?这就是调度器切换线程的原理

上面我们已经介绍了是通过Dispatchers指定协程运行的线程,通过interceptContinuation在协程恢复前进行拦截,从而切换线程
带着这些前置知识,我们一起来看下协程启动的具体流程,明确下协程切换线程源码具体实现

2. 协程线程切换源码分析

2.1 launch方法解析

我们首先看一下协程是怎样启动的,传入了什么参数

public fun CoroutineScope.launch(context: CoroutineContext = EmptyCoroutineContext,start: CoroutineStart = CoroutineStart.DEFAULT,block: suspend CoroutineScope.() -> Unit
): Job {val newContext = newCoroutineContext(context)val coroutine = if (start.isLazy)LazyStandaloneCoroutine(newContext, block) elseStandaloneCoroutine(newContext, active = true)coroutine.start(start, coroutine, block)return coroutine
}

总共有3个参数:
1.传入的协程上下文
2.CoroutinStart启动器,是个枚举类,定义了不同的启动方法,默认是CoroutineStart.DEFAULT
3.block就是我们传入的协程体,真正要执行的代码

这段代码主要做了两件事:
1.组合新的CoroutineContext
2.再创建一个 Continuation

2.1.1 组合新的CoroutineContext

public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {val combined = coroutineContext + contextval debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combinedreturn if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)debug + Dispatchers.Default else debug
}

从上面可以提炼出以下信息:
1.会将launch方法传入的contextCoroutineScope中的context组合起来
2.如果combined中没有拦截器,会传入一个默认的拦截器,即Dispatchers.Default,这也解释了为什么我们没有传入拦截器时会有一个默认切换线程的效果

2.1.2 创建一个Continuation

val coroutine = if (start.isLazy)LazyStandaloneCoroutine(newContext, block) elseStandaloneCoroutine(newContext, active = true)coroutine.start(start, coroutine, block)

默认情况下,我们会创建一个StandloneCoroutine
值得注意的是,这个coroutine其实是我们协程体的complete,即成功后的回调,而不是协程体本身
然后调用coroutine.start,这表明协程开始启动了

2.2 协程的启动

public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {initParentJob()start(block, receiver, this)
}

接着调用CoroutineStartstart来启动协程,默认情况下调用的是CoroutineStart.Default

经过层层调用,最后到达了:

internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(receiver: R, completion: Continuation<T>) =runSafely(completion) {// 外面再包一层 CoroutinecreateCoroutineUnintercepted(receiver, completion)// 如果需要,做拦截处理.intercepted()// 调用 resumeWith 方法      .resumeCancellableWith(Result.success(Unit))}

这里就是协程启动的核心代码,虽然比较短,却包括3个步骤:
1.创建协程体Continuation
2.创建拦截 Continuation,即DispatchedContinuation
3.执行DispatchedContinuation.resumeWith方法

2.3 创建协程体Continuation

调用createCoroutineUnintercepted,会把我们的协程体即suspend block转换成Continuation,它是SuspendLambda,继承自ContinuationImpl
createCoroutineUnintercepted方法在源码中找不到具体实现,不过如果你把协程体代码反编译后就可以看到真正的实现
详情可见:字节码反编译

2.4 创建DispatchedContinuation

public actual fun <T> Continuation<T>.intercepted(): Continuation<T> =(this as? ContinuationImpl)?.intercepted() ?: this//ContinuationImpl
public fun intercepted(): Continuation<Any?> =intercepted?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this).also { intercepted = it }     //CoroutineDispatcher
public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =DispatchedContinuation(this, continuation)           

从上可以提炼出以下信息
1.interepted是个扩展方法,最后会调用到ContinuationImpl.intercepted方法
2.在intercepted会利用CoroutineContext,获取当前的拦截器
3.因为当前的拦截器是CoroutineDispatcher,因此最终会返回一个DispatchedContinuation,我们其实也是利用它实现线程切换的
4.我们将协程体的Continuation传入DispatchedContinuation,这里其实用到了装饰器模式,实现功能的增强

这里其实很明显了,通过DispatchedContinuation装饰原有协程,在DispatchedContinuation里通过调度器处理线程切换,不影响原有逻辑,实现功能的增强

2.5 拦截处理

    //DispatchedContinuationinline fun resumeCancellableWith(result: Result<T>,noinline onCancellation: ((cause: Throwable) -> Unit)?) {val state = result.toState(onCancellation)if (dispatcher.isDispatchNeeded(context)) {_state = stateresumeMode = MODE_CANCELLABLEdispatcher.dispatch(context, this)} else {executeUnconfined(state, MODE_CANCELLABLE) {if (!resumeCancelled(state)) {resumeUndispatchedWith(result)}}}}

上面说到了启动时会调用DispatchedContinuationresumeCancellableWith方法
这里面做的事也很简单:
1.如果需要切换线程,调用dispatcher.dispatcher方法,这里的dispatcher是通过CoroutineConext取出来的
2.如果不需要切换线程,直接运行原有线程即可

2.5.2 调度器的具体实现

我们首先明确下,CoroutineDispatcher是通过CoroutineContext取出来的,这也是协程上下文作用的体现
CoroutineDispater官方提供了四种实现:Dispatchers.Main,Dispatchers.IO,Dispatchers.Default,Dispatchers.Unconfined
我们一起简单看下Dispatchers.Main的实现

internal class HandlerContext private constructor(private val handler: Handler,private val name: String?,private val invokeImmediately: Boolean
) : HandlerDispatcher(), Delay {public constructor(handler: Handler,name: String? = null) : this(handler, name, false)//...override fun dispatch(context: CoroutineContext, block: Runnable) {// 利用主线程的 Handler 执行任务handler.post(block)}
}

可以看到,其实就是用handler切换到了主线程
如果用Dispatcers.IO也是一样的,只不过换成线程池切换了

如上所示,其实就是一个装饰模式
1.调用CoroutinDispatcher.dispatch方法切换线程
2.切换完成后调用DispatchedTask.run方法,执行真正的协程体

3 delay是怎样切换线程的?

上面我们介绍了协程线程调度的基本原理与实现,下面我们来回答几个小问题
我们知道delay函数会挂起,然后等待一段时间再恢复。
可以想象,这里面应该也涉及到线程的切换,具体是怎么实现的呢?

public suspend fun delay(timeMillis: Long) {if (timeMillis <= 0) return // don't delayreturn suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->// if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.if (timeMillis < Long.MAX_VALUE) {cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)}}
}internal val CoroutineContext.delay: Delay get() = get(ContinuationInterceptor) as? Delay ?: DefaultDelay

Dealy的代码也很简单,从上面可以提炼出以下信息
delay的切换也是通过拦截器来实现的,内置的拦截器同时也实现了Delay接口
我们来看一个具体实现

internal class HandlerContext private constructor(private val handler: Handler,private val name: String?,private val invokeImmediately: Boolean
) : HandlerDispatcher(), Delay {override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {// 利用主线程的 Handler 延迟执行任务,将完成的 continuation 放在任务中执行val block = Runnable {with(continuation) { resumeUndispatched(Unit) }}handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY))continuation.invokeOnCancellation { handler.removeCallbacks(block) }}//..
}

1.可以看出,其实也是通过handler.postDelayed实现延时效果的
2.时间到了之后,再通过resumeUndispatched方法恢复协程
3.如果我们用的是Dispatcher.IO,效果也是一样的,不同的就是延时效果是通过切换线程实现的

4. withContext是怎样切换线程的?

我们在协程体内,可能通过withContext方法简单便捷的切换线程,用同步的方式写异步代码,这也是kotin协程的主要优势之一

    fun test(){viewModelScope.launch(Dispatchers.Main) {print("1:" + Thread.currentThread().name)withContext(Dispatchers.IO){delay(1000)print("2:" + Thread.currentThread().name)}print("3:" + Thread.currentThread().name)}}//1,2,3处分别输出main,DefaultDispatcher-worker-1,main

可以看出这段代码做了一个切换线程然后再切换回来的操作,我们可以提出两个问题
1.withContext是怎样切换线程的?
2.withContext内的协程体结束后,线程怎样切换回到Dispatchers.Main?

public suspend fun <T> withContext(context: CoroutineContext,block: suspend CoroutineScope.() -> T
): T {  return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->// 创建新的contextval oldContext = uCont.contextval newContext = oldContext + context....//使用新的Dispatcher,覆盖外层val coroutine = DispatchedCoroutine(newContext, uCont)coroutine.initParentJob()//DispatchedCoroutine作为了complete传入block.startCoroutineCancellable(coroutine, coroutine)coroutine.getResult()}
}private class DispatchedCoroutine<in T>(context: CoroutineContext,uCont: Continuation<T>
) : ScopeCoroutine<T>(context, uCont) {//在complete时会会回调override fun afterCompletion(state: Any?) {afterResume(state)}override fun afterResume(state: Any?) {//uCont就是父协程,context仍是老版context,因此可以切换回原来的线程上uCont.intercepted().resumeCancellableWith(recoverResult(state, uCont))}
}

这段代码其实也很简单,可以提炼出以下信息
1.withContext其实就是一层Api封装,最后调用到了startCoroutineCancellable,这就跟launch后面的流程一样了,我们就不继续跟了
2.传入的context会覆盖外层的拦截器并生成一个newContext,因此可以实现线程的切换
3.DispatchedCoroutine作为complete传入协程体的创建函数中,因此协程体执行完成后会回调到afterCompletion
4.DispatchedCoroutine中传入的uCont是父协程,它的拦截器仍是外层的拦截器,因此会切换回原来的线程中

总结

本文主要回答了kotlin协程到底是怎么切换线程的这个问题,并对源码进行了分析
简单来讲主要包括以下步骤:
1.向CoroutineContext添加Dispatcher,指定运行的协程
2.在启动时将suspend block创建成Continuation,并调用intercepted生成DispatchedContinuation
3.DispatchedContinuation就是对原有协程的装饰,在这里调用Dispatcher完成线程切换任务后,resume被装饰的协程,就会执行协程体内的代码了

其实kotlin协程就是用装饰器模式实现线程切换的
看起来似乎有不少代码,但是真正的思路其实还是挺简单的,这大概就是设计模式的作用吧

最后

小编分享一些 Android 开发相关的学习文档、面试题、Android 核心笔记等等文档,希望能帮助到大家学习提升,如有需要参考的可以直接去我 CodeChina地址:https://codechina.csdn.net/u012165769/Android-T3 访问查阅。如果本文对你有所帮助,欢迎点赞收藏~


http://chatgpt.dhexx.cn/article/rCjUX2w3.shtml

相关文章

进程和线程、协程的区别

一、进程 进程是程序一次动态执行的过程&#xff0c;是程序运行的基本单位。每个进程都有自己的独立内存空间&#xff0c;不同进程通过进程间通信来通信。进程占据独立的内存&#xff0c;所以上下文进程间的切换开销&#xff08;栈、寄存器、页表、文件句柄等&#xff09;比较…

进程、线程与协程的比较

进程、线程和协程是三个在多任务处理中常听到的概念&#xff0c;三者各有区别又相互联系。 一、并行和并发 在介绍进程、线程和协程这三个概念之前&#xff0c;有两个操作系统中的相关概念需要简单解释一下&#xff1a;并行和并发。 并行&#xff1a;指多个任务同时执行。 并…

进程、线程、协程

进程、线程、协程 一、概念与区分 1、进程 进程是程序一次动态执行的过程&#xff0c;是程序运行的基本单位。每个进程都有自己的独立内存空间&#xff0c;不同进程通过进程间通信来通信。进程占据独立的内存&#xff0c;所以上下文进程间的切换开销&#xff08;栈、寄存器、…

终于明白:有了线程,为什么还要有协程?

并发的发展历史 其实&#xff0c;在早期计算机并没有包含操作系统&#xff0c;这个时候&#xff0c;这个计算机只跑一个程序&#xff0c;这个程序独享计算机的所有资源&#xff0c;这个时候不存在什么并发问题&#xff0c;但是对计算机的资源来说&#xff0c;确实是一种浪费。…

Java中的多线程(线程间通信)

/学习笔记/ 线程间通信&#xff1a; 多个线程在处理同一资源&#xff0c;但是任务却不同。 先看一个例子&#xff0c;采用两个线程执行进行输入和输出任务&#xff1a; //资源class Resource{String name;String sex;}//输入class Input implements Runnable{Resource r ;// …

协程和线程的区别、协程原理与优缺点分析、在Java中使用协程

文章目录 什么是协程协程的优点与缺点协程实现原理.协程与线程在不同编程语言的实现在Java中使用协程Kilim介绍Kilim整合Java,使用举例 小总结 什么是协程 相对于协程&#xff0c;你可能对进程和线程更为熟悉。进程一般代表一个应用服务&#xff0c;在一个应用服务中可以创建多…

进程、线程和协程之间的区别和联系

文章目录 一、进程二、线程三、进程和线程的区别与联系四、一个形象的例子解释进程和线程的区别五、进程/线程之间的亲缘性六、协程 一、进程 进程&#xff0c;直观点说&#xff0c;保存在硬盘上的程序运行以后&#xff0c;会在内存空间里形成一个独立的内存体&#xff0c;这个…

简单了解线程和协程(C#)

1.为什么需要线程和协程&#xff1a; &#xff08;1&#xff09;使程序中的任务可以并发执行&#xff0c;让程序同时处理多个任务&#xff0c;提高程序的运行效率和响应速度 &#xff08;2&#xff09;线程和协程可以共享同一个进程的资源&#xff0c;避免多个进程之间的资源浪…

线程与协程

线程与协程 概念进程【进程间通信&#xff08;IPC&#xff09;】 线程协程 区别场景计算密集型IO密集型两种操作如何优化哪些语言对多协程的支持 概念 进程 二进制可执行文件在计算机内存里的一个运行实例。比如.exe文件是个类&#xff0c;进程就是new出来的那个实例&#xf…

总结:协程与线程

一、介绍 本文主要梳理下进程&#xff0c;线程&#xff0c;协程的概念、区别以及使用场景的选择。 二、进程 我们知道&#xff0c;一切的软件都是跑在操作系统上&#xff0c;真正用来干活 (计算) 的是 CPU。早期的操作系统每个程序就是一个进程&#xff0c;知道一个程序运行完…

Java线程协程

目录 线程的实现&#xff08;OS&&JVM&#xff09; 1.内核线程实现 2.用户线程实现 3.混合实现 4.Java线程的实现 ——如何实现不受Java虚拟机规范的约束 Java线程调度——系统自动完成&#xff08;可能被干预&#xff09; Java线程状态转换 内核线程的局限 协…

CDH6.3.2安装文档

正式安装之前&#xff0c;先说明一下CDH是基于Apache Hadoop及相关项目的发行版。CDH通过WEB界面管理&#xff0c;并提供了hadoop的两个核心功能&#xff1a;可扩展存储和分布式计算&#xff0c;以及企业级的重要功能。CDH是遵循Apache-licensed的开源软件&#xff0c;提供了基…

CDH数仓项目(一) —— CDH安装部署搭建详细流程

0 说明 本文以CDH搭建数据仓库&#xff0c;基于三台阿里云服务器从零开始搭建CDH集群&#xff0c;节点配置信息如下&#xff1a; 节点内存安装服务角色chen10216Gcloudera-scm-serverchen1038Gcloudera-scm-agentchen1048Gcloudera-scm-agent 上传需要用到的安装包&#xff…

CDH5.8安装说明

#CDH5.8安装说明 (Hadoop) 使用过Ambari&#xff0c;不知道是因为没有商业运作支撑还是社区活跃度有限&#xff0c;总体管理能力只能算凑合。很多Hadoop组件版本都不高&#xff0c;Spark也才1.3.X&#xff0c;Sqoop还是1.4.6.2.3&#xff0c;相对版本都比较低&#xff0c;而且…

大数据CDH安装详细教程

1.环境准备 1.1 服务器配置(理想配置) 1.2 修改主机名和hosts文件(所有节点) [roothadoop001 ~]# vim /etc/hosts vim /etc/hostname1.3 关闭防火墙 systemctl stop firewalld systemctl disable firewalld1.4 SSH免密登录(主节点) ssh-keygen -t rsa #分发到所有节点 ssh…

CDH6安装

官方文档 https://www.cloudera.com/documentation/enterprise/6/6.0/topics/installation.html 安装之前 JDK兼容性在不同的Cloudera Manager和CDH版本中也有所不同。某些版本的CDH 5与JDK 7和JDK 8兼容。在这种情况下&#xff0c;请确保所有服务都部署在同一主要版本上。例…

Cloudera(CDH) 简介和在线安装

实验背景 笔者需要维护线上的hadoop集群环境&#xff0c;考虑在本地搭建一套类似的hadoop集群&#xff0c;便于维护与管理。 Cloudera 简介 经过搜索发现Cloudera产品很适合笔者当前需求&#xff0c;于是开始研究Cloudera&#xff08;CDH&#xff09;的安装与使用&#xff0c;参…

CDH6.3.1安装

CDH6.3.1安装遇到很多问题&#xff0c;我想主要是由于条件有限&#xff0c;毕竟自己的电脑内存不如专业集群的内存大&#xff08;如果是内存和硬盘充足&#xff0c;有些是可以避免的&#xff0c;甚至不会出现报错的情况&#xff09;&#xff0c;这里就介绍一下我用VMware安装的…

CDH安装配置

Cloudera5.14配置 准备工作 软件下载软件安装 JDK安装 所有节点 安装环境变量配置 sudo vim /etc/profile export JAVA_HOME/usr/java/default export PATH$JAVA_HOME/bin:$PATH 使用root用户 echo "JAVA_HOME/usr/java/default" >> /etc/environment …

CDH 6.3.2 安装(一)

目录 一、CDH框架介绍 1、CDH介绍 2、CDH官方网址 3、CDH官方文档 4、CDH集群扩容 5、CDH硬件要求 6、CDH k8s服务开启 二、CDH依赖安装 1、安装通用依赖 2、网络工具安装 3、防火墙服务安装 4、进程树形工具安装 5、其它依赖安装 三、Linux系统配置 1、主机名配…