使用kotlin的协程一段时间后,我们或多或少会产生一些疑问:协程和线程有什么关系?协程之间到底怎么来回传递的?协程真的比线程(池)好吗?
初窥
首先我们从最简单协程开始:
fun main() {
GlobalScope.launch(Dispatchers.IO) {
val aaa = async {
println("aaa-")
}
aaa.await()
println("bbb-")
}
//sleep只是防止main挂掉
Thread.sleep(100_000)
}
我们遵循“如果看不懂,那就看一下字节码或者转成java”的套路,看一下java代码大概的样子:
public static final void main() {
//协程开始
BuildersKt.launch$default((CoroutineScope) GlobalScope.INSTANCE, (CoroutineContext) Dispatchers.getIO(), (CoroutineStart)null,
//这里传了个回调(续体)
(Function2)(new Function2((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
//很明显,这是协程刚开始执行的代码
Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
//label默认是0,所以走初始化和创建aaa的回调(续体)
ResultKt.throwOnFailure($result);
CoroutineScope $this$launch = (CoroutineScope)this.L$0;
System.out.println("开始");
Deferred aaa = BuildersKt.async$default($this$launch, (CoroutineContext)null, (CoroutineStart)null, (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:
//aaa的代码执行
ResultKt.throwOnFailure(var1);
System.out.println("aaa-");
return Unit.INSTANCE;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
}
...
}), 3, (Object)null);
//label赋值1,如果下次再调用就会走case 1了
this.label = 1;
//获取aaa的实时结果,并且把自己(回调)传了进去
if (aaa.await(this) == var5) {
//如果aaa告诉你等一会再给结果(参考var5赋值),则代码结束
return var5;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
}
//上面相当于传给了aaa一个回调(续体),当aaa执行完成后会再次调用invokeSuspend
//在case 1里,如果aaa正常完成,则会进到这里
//最终结束
System.out.println("结束");
return Unit.INSTANCE;
}
...
}), 2, (Object)null);
Thread.sleep(100000L);
}
因为上面有label这种状态机制(状态机),不方便理解,我们画成流程图
依据上面也能很轻松就能推理出,如果我需要await aaa、bbb、ccc三个,我们只需要在“准备执行结束代码”前面塞上和aaa一样的bbb、ccc逻辑即可,如下
如果想深入查看了解源码请移步:Kotlin协程实现原理 - Giagor - 博客园 (cnblogs.com)
困境
看了上面的基本原理,有没有产生个疑问:它和线程池有什么区别?
我们先仿照上面用线程池实现一下:
//线程池版
fun main2() {
//ThreadPool为线程池
ThreadPool.run {
println("开始")
ThreadPool.run {
println("aaa-")
ThreadPool.run {
println("结束")
}
}
}
//sleep只是防止main挂掉
Thread.sleep(100_000)
}
当然如果涉及到aaa、bbb、ccc都并发的情况下,我们需要重新革命一下代码逻辑
//线程池版
fun main2() {
//ThreadPool为线程池
ThreadPool.run {
println("开始")
ThreadPool.run {
println("aaa-")
callEnd()
}
ThreadPool.run {
println("bbb-")
callEnd()
}
ThreadPool.run {
println("ccc-")
callEnd()
}
}
//sleep只是防止main挂掉
Thread.sleep(100_000)
}
val callCount = AtomicInteger(0)
fun callEnd() {
val count = callCount.incrementAndGet()
//用count来计数,当三个都成功时结束
if (count == 3) {
ThreadPool.run {
println("结束")
}
}
}
虽然代码可能比协程多了一点,但它丝毫不影响我对协程的推测:协程是一个共用线程池。翻开“Dispatchers.IO”的源码——没错它就是一个公共线程池。
是不是有一种:把协程吹得那么高大尚,就这?
破局
还记得近期大火的“三体”吗,汪淼花了大量时间破解三体的太阳之谜,而最终的答案在哪里呢?没错“三体”就是三个物体因力学关系互相影响而无法归纳他们的运动轨迹。而我们协程的全称叫“协同程序”,所以作为协同程序它的目标是:
1. 轻量:协程全局共享线程,消耗资源较少
2.灵活:轻松编写非阻塞式异步代码
3.简洁:避免回调地狱,避免过多改动代码,更友好简单的代码
4.可控:启动、暂停、恢复、异常等均可自行管理控制
释然
协程和线程池目标不同,并且各有所长,只不过大部分情况下协程比较占优罢了。
线程cpu执行的基本单元,和上面两个的概念完全不同,并且多cpu架构的协程必然存在线程池。