协程使用说明:https://blog.csdn.net/tmacfrank/article/details/145084326
协程原理分析:
协程是用户态的线程运行框架,并不是直接操作操作系统级别的线程对象,大部分情况下可以不涉及线程的销毁、创建、回收、线程调度等操作系统级别的操作,所以协程比线程对操作系统的资源消耗更少,运行效率也更高
CoroutineScope(Dispatchers.Default).launch {
println("${Thread.currentThread().name}: test")
}
Thread.sleep(100)
输出打印:
DefaultDispatcher-worker-1: test
上面为最简单的协程使用例子,作用域(CoroutineScope)指定一种类型的派发器(Dispatchers),lambda表达式里面的任务就会在所指定的线程/线程池里面执行,launch返回一个 Job 对象,Job 对象可以获取协程任务的运行状态、开始或者取消协程
一:协程上下文(CoroutineContext)
协程上下文的作用:资源获取,配置管理等工作,是执行环境的通用数据资源的统一管理者。是协程运行必须设置的参数。
以下对象本质上都是协程上下文
- Job 协程句柄
- CoroutineName 协程名称
- CoroutineDispatcher 协程调度器
- CoroutineExceptionHandler 协程异常处理器
launch函数里面实现了操作符重载,支持以上多种类型的上下文进行 + 操作,结合后的协程上下文变成 [xxxxxxx,xxxxxxxx,xxxxxxxx] 形式
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) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
第一个参数是协程上下文,可以指定 Dispatcher 类型
派发器的分类:
* Dispatchers.Default : 默认的派发器,会将协程派发到默认的线程池进行调度和执行
* Dispatchers.Main : 主线程派发器,会将协程派发到主线程调度和执行,Kotlin 默认没有实现该调度器,需要实际的接入方提供具体的实现,针对 Android 开发来说,需要额外添加 kotlinx-coroutines-android 的运行时依赖
* Dispatchers.Unconfined : 未限定协程执行线程的派发器,该派发器会在调用线程中执行该协程,当协程被挂起后,恢复时所在的线程由挂起函数决定(在挂起时,由挂起函数决定要将该协程派发到哪个线程执行)
* Dispatchers.IO : IO 派发器,和 Default 派发器共用执行线程池,默认为 64 和虚拟机中可用的处理器核心数中的最大值
第二个参数是调度策略,可以指定 CoroutineStart 类型
调度策略分类:
* CoroutineStart.DEFAULT : 默认的调度策略,会立刻将协程调度进入目标线程/线程池中的任务队列,等待执行,在实际执行之前可以通过 Job.cancel 方法取消。
* CoroutineStart.LAZY: 懒调度策略,只有当协程需要被执行的时候才会进行调度,调用方可以通过 Job.start 方法来触发调度
* CoroutineStart. ATOMIC: 自动调度,和 DEFAULT 策略类似,会立刻调度协程到任务队列中,区别在于该调度策略在协程实际执行前无法取消
* CoroutineStart.UNDISPATCHED: 立刻在当前调用线程中执行该协程,直到协程遇到第一个挂起函数,恢复时根据协程上下文中的派发器来决定要将协程派发到哪个线程/线程池中继续执行。类似于 Unconfined 派发器的作用,区别在于该策略在协程挂起并恢复后的执行线程由协程派发器决定。
二:调度原理
关键路径
CoroutineStart.invoke -> CancellableKt.startCoroutineCancellable ->
生成 Continuation 对象 -> resumeCancellableWith -> 进入 dispatch 任务分发流程 ->
CoroutineScheduler. dispatch -> 创建任务、将任务添加到队列、执行任务 -> DispatchedTask.run ->
BaseContinuationImpl.resumeWith -> BaseContinuationImpl. invokeSuspend -> 执行 挂起函数,就是外部传进来的 lambda 表达式
反编译后,可以看到最终执行的是 invokSuspend 函数
public final class CoroutinePrincipleKt {
public static final void main() {
Job job = BuildersKt.launch(CoroutineScopeKt.CoroutineScope((CoroutineContext)EmptyCoroutineContext.INSTANCE), (CoroutineContext)Dispatchers.getIO(), CoroutineStart.LAZY, (Function2)(new Function2((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object var1) {
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch (this.label) {
case 0:
ResultKt.throwOnFailure(var1);
StringBuilder var10000 = new StringBuilder();
Thread var10001 = Thread.currentThread();
Intrinsics.checkNotNullExpressionValue(var10001, "Thread.currentThread()");
String var2 = var10000.append(var10001.getName()).append(": test11").toString();
System.out.println(var2);
return Unit.INSTANCE;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
}
@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkNotNullParameter(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}));
job.start();
Thread.sleep(100L);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
三:挂起、恢复
协程之所以高效的原因
CoroutineScope(Dispatchers.Default).launch {
println("${Thread.currentThread().id} : coroutine1 log 1")
delay(200)
print("${Thread.currentThread().id} : coroutine1 log 2")
}
Thread.sleep(80)
CoroutineScope(Dispatchers.Default).launch {
println("${Thread.currentThread().id} : coroutine2 log 1")
}
Thread.sleep(500)
运行上面的代码后,会出现以下打印
10 : coroutine1 log 1
10 : coroutine2 log 1
10 : coroutine1 log 2
从打印的结果看到,协程做到了在不阻塞当前线程的情况下,实现了任务的挂起,另一个任务可以运行在相同的线程下,并且挂起结束后,挂起任务可以继续在当前线程运行。
用同步的方式,实现了异步的效果,中间不涉及线程切换等操作系统级别的操作。
问题一:为什么协程会知道需要执行另一个任务?
DefaultExecutor 内部有单线程轮询堆顶任务,通过 delay 时间比较,确保能及时运行任务队列里面的任务。如果当前任务等待时间还没到,就会运行其他等待时间更少或者无需等待的任务
问题二:协程怎么知道恢复后,到底需要从哪里继续执行剩余逻辑?
BuildersKt.launch$default(CoroutineScopeKt.CoroutineScope((CoroutineContext)Dispatchers.getDefault()), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
StringBuilder var10000;
Thread var10001;
String var2;
switch (this.label) {
case 0:
ResultKt.throwOnFailure($result);
var10000 = new StringBuilder();
var10001 = Thread.currentThread();
Intrinsics.checkNotNullExpressionValue(var10001, "Thread.currentThread()");
var2 = var10000.append(var10001.getId()).append(" : coroutine1 log 1").toString();
System.out.println(var2);
// 执行完第一行 log 的打印,准备执行 delay 函数挂起协程,这里把 label 设置为 1
this.label = 1;
if (DelayKt.delay(200L, this) == var3) {
return var3;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
var10000 = new StringBuilder();
var10001 = Thread.currentThread();
Intrinsics.checkNotNullExpressionValue(var10001, "Thread.currentThread()");
var2 = var10000.append(var10001.getId()).append(" : coroutine1 log 2").toString();
System.out.print(var2);
return Unit.INSTANCE;
}
@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkNotNullParameter(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}), 3, (Object)null);
执行完挂起点后,会将 label 自增,实现恢复后执行挂起点之后的逻辑
本质上就是维护一个状态机,通过 label 字段实现多个挂起点的逻辑还原
四、异常处理
// 创建一个协程异常处理上下文
val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
val coroutineName = coroutineContext[CoroutineName]?.name
println("$coroutineName occurred exception: $throwable")
}
CoroutineScope(Dispatchers.Default).launch(coroutineExceptionHandler + CoroutineName("My coroutine")) {
println("${Thread.currentThread().name}: coroutine test 1")
throw RuntimeException("test exception")
}
Thread.sleep(1000)
打印结果:
DefaultDispatcher-worker-1: coroutine test 1
My coroutine occurred exception: java.lang.RuntimeException: test exception
如果外部不设置异常处理逻辑,协程内部会有默认的异常兜底
五、协程作用域
CoroutineScope 的作用是方便批量管理协程,每一个作用域都有一个根 Job,作用域内创建的协程会作为子 Job 添加到根 Job 上。
如果根 Job 被取消,则这个作用域下的 Job 对象,也会被取消。
作用域的分类:
* runBlocking:顶层函数,它的第二个参数为接收者是CoroutineScope的函数字面量,可启动协程。但是它会阻塞当前线程,主要用于测试。
* GlobalScope:全局协程作用域,通过GlobalScope创建的协程不会有父协程,可以把它称为根协程。它启动的协程的生命周期只受整个应用程序的生命周期的限制,且不能取消,在运行时会消耗一些内存资源,这可能会导致内存泄露,所以仍不适用于业务开发。
* coroutineScope:创建一个独立的协程作用域,直到所有启动的协程都完成后才结束自身。它是一个挂起函数,需要运行在协程内或挂起函数内。当这个作用域中的任何一个子协程失败时,这个作用域失败,所有其他的子程序都被取消。为并行分解工作而设计的。
* supervisorScope:与coroutineScope类似,不同的是子协程的异常不会影响父协程,也不会影响其他子协程。(作用域本身的失败(在block或取消中抛出异常)会导致作用域及其所有子协程失败,但不会取消父协程。)
* MainScope:为UI组件创建主作用域。一个顶层函数,上下文是SupervisorJob() + Dispatchers.Main,说明它是一个在主线程执行的协程作用域,通过cancel对协程进行取消。推荐使用。
如果是Android运行环境,还有
* lifecycleScope:Lifecycle Ktx库提供的具有生命周期感知的协程作用域,与Lifecycle绑定生命周期,生命周期被销毁时,此作用域将被取消。会与当前的UI组件绑定生命周期,界面销毁时该协程作用域将被取消,不会造成协程泄漏,推荐使用。
* viewModelScope:与lifecycleScope类似,与ViewModel绑定生命周期,当ViewModel被清除时,这个作用域将被取消。推荐使用。
以上两个 Android 环境的作用域,会和界面、ViewModel 绑定,如果界面销毁、ViewModel 销毁,自身的作用域也会销毁