被C#启发

在C#中,用async和await关键字可以实现挂起和恢复,因而Kt协程的原型像下面这样

后来发现没必要使用这两个关键字——async , await,直接用一个 suspend 关键字代替就好了啊。但是团队一开始还不太敢这么做,因为很多其他受到C#的 async await启发的并发结构都没有这样做——在外观上取消了普通函数和挂起函数的差异,直到Go语言的出现让它们坚定了自己的想法。

from Go 1

从Go中学到了很多,比如 delaylaunch都在Go中有类似的表达。

和Go的不同,UI编程如何搞

一开始的原型是有这么一个函数

1
2
3
4
launchUI{

// invoke suspend function here
}

后来抽象出了 CoroutineContext

1
2
3
launch(UI) {
// invoke suspend function here
}

不是新创建一个函数,而是通过参数的方式指定 上下文

取消机制

C#和Go都需要为取消行为显式地写很多样板代码。Kotlin想避免这一点。

不同于Go,要显式地传递context上下文,Kotlin创建了 Lifetime生命周期, Lifetime被定义为上下文的一个元素。

后来名字更改为 Job

所以Job的概念来自于生命周期,而生命周期来自于如何创建取消机制。

通过 Job.cancel可以取消一个协程

并行分解

问题来到如何面对 concurrent decomposition

1
2
3
4
5
6
launch {

}
launch {

}

如何同时取消两个子协程?

一开始的原型如下

但是这带来了 样板代码。job成为了需要显式传递的token。而Kotlin协程想避免一切显式传递

所以CoroutineContext干脆设计成隐式传递

错误传递

因为一开始Job的设计只有 Active 和 Complete 这两个状态,所以它没有办法处理 子协程(并发运行) 同时报错的情况。

如上图,在Coroutine1报错,cancel了parent,但是Coroutine2也报错了,也尝试cancel parent(parent 被 cancel 之后还没来得及取消 Coroutine2 呢)。

所以有必要更改Job的模型,增加它的状态,让它 当所有child完成才完成

所以下面代码中的 外层花括号 还隐含有 等待内部开的子协程都完成才完成的 含义

1
2
3
4
5
6
7
8
val job = launch {
launch{

}
launch{

}
}

并行分解在函数里的问题

在原型中(现在已不能这么写),如下所示,这个函数具有 side effect,它如果失败,会取消 parent job,但是会 return normally。

解决办法就是 强制 使用一个作用域包裹住 挂起函数里的 launch

1
2
3
4
5
6
7
suspend fun sayHelloWorld() {
withScope {
launch(coroutineContext) {
// do something
}
}
}

但是这种方式容易出错,因为开发者容易忘记写 withScope,而代码仍然可以正常编译。所以解决办法就是把 launch 设计成 CoroutineScope 的拓展函数,launch只能在一个作用域里面调用,所以下面的代码直接编译错误

1
2
3
4
5
suspend fun funcTest() {
launch{ // compiled error

}
}

launch的函数签名如下

1
2
3
4
5
6
7
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
// ...
}

后来把 withScope命名为 coroutineScope。没有副作用了,要么正常返回,要么抛出异常。

在这个作用域内可以尽情地玩耍

结构化并发

这也就顺其自然地引出了 Structure currency 的概念,这个名称来源于一篇博客,那篇博客讲了现代并发编程的一些问题,说类似 goto 语句之类的并发编程原语破坏了程序结构,总是跳来跳去违反直觉,人类难以理解。它们应该像 if for这些一样可以被 merge 在一起。而这就是Kotlin团队做的。

这也是为什么CoroutineExceptionHandler需要在顶层作用域上定义,因为 parent always watis for childen completion,这句话是结构化并发的精髓。

写代码的时候应该尽量写符合 结构化并发 的代码。