Roman Elizarov的讲座笔记
被C#启发
在C#中,用async和await关键字可以实现挂起和恢复,因而Kt协程的原型像下面这样
后来发现没必要使用这两个关键字——async
, await
,直接用一个 suspend
关键字代替就好了啊。但是团队一开始还不太敢这么做,因为很多其他受到C#的 async await启发的并发结构都没有这样做——在外观上取消了普通函数和挂起函数的差异,直到Go语言的出现让它们坚定了自己的想法。
from Go 1
从Go中学到了很多,比如 delay
,launch
都在Go中有类似的表达。
和Go的不同,UI编程如何搞
一开始的原型是有这么一个函数
1 | launchUI{ |
后来抽象出了 CoroutineContext
1 | launch(UI) { |
不是新创建一个函数,而是通过参数的方式指定 上下文。
取消机制
C#和Go都需要为取消行为
显式地写很多样板代码。Kotlin想避免这一点。
不同于Go,要显式地传递context上下文,Kotlin创建了 Lifetime
生命周期, Lifetime
被定义为上下文的一个元素。
后来名字更改为 Job
。
所以Job
的概念来自于生命周期,而生命周期来自于如何创建取消机制。
通过 Job.cancel
可以取消一个协程
并行分解
问题来到如何面对 concurrent decomposition
1 | launch { |
如何同时取消两个子协程?
一开始的原型如下
但是这带来了 样板代码。job成为了需要显式传递的token。而Kotlin协程想避免一切显式传递。
所以CoroutineContext
干脆设计成隐式传递
错误传递
因为一开始Job的设计只有 Active 和 Complete 这两个状态,所以它没有办法处理 子协程(并发运行) 同时报错的情况。
如上图,在Coroutine1报错,cancel了parent,但是Coroutine2也报错了,也尝试cancel parent(parent 被 cancel 之后还没来得及取消 Coroutine2 呢)。
所以有必要更改Job的模型,增加它的状态,让它 当所有child完成才完成。
所以下面代码中的 外层花括号 还隐含有 等待内部开的子协程都完成才完成的 含义
1 | val job = launch { |
并行分解在函数里的问题
在原型中(现在已不能这么写),如下所示,这个函数具有 side effect,它如果失败,会取消 parent job,但是会 return normally。
解决办法就是 强制 使用一个作用域包裹住 挂起函数里的 launch
1 | suspend fun sayHelloWorld() { |
但是这种方式容易出错,因为开发者容易忘记写 withScope,而代码仍然可以正常编译。所以解决办法就是把 launch
设计成 CoroutineScope
的拓展函数,launch只能在一个作用域里面调用,所以下面的代码直接编译错误
1 | suspend fun funcTest() { |
launch
的函数签名如下
1 | public fun CoroutineScope.launch( |
后来把 withScope
命名为 coroutineScope
。没有副作用了,要么正常返回,要么抛出异常。
在这个作用域内可以尽情地玩耍
结构化并发
这也就顺其自然地引出了 Structure currency 的概念,这个名称来源于一篇博客,那篇博客讲了现代并发编程的一些问题,说类似 goto
语句之类的并发编程原语破坏了程序结构,总是跳来跳去违反直觉,人类难以理解。它们应该像 if
for
这些一样可以被 merge 在一起。而这就是Kotlin团队做的。
这也是为什么CoroutineExceptionHandler需要在顶层作用域上定义,因为 parent always watis for childen completion,这句话是结构化并发的精髓。
写代码的时候应该尽量写符合 结构化并发 的代码。