之前写了一篇协程速通,发现在阅读协程代码的时候还是很会卡住,细想了一下,发现是很多协程的招式看不懂。所以,非常有必要学习并理解一下基本的API,以及API里面的一些运作方式。
runBlocking
public actual fun <T> runBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T
看函数参数 block 可知,这个函数返回的就是我们在 block 里面返回的那个值。
函数注释上说,不建议使用这个方法,这个函数一般只用于 main 函数和测试代码中。但是我们学习协程会经常用到这个方法,所以还是要介绍一下。

里面的代码比较多,但是我们只需要关注两个位置,第一个是 CoroutineContext ,第二个是 BlockingCoroutine。这两个东西我们后面文章会说到,现在只需要了解这个函数的作用即可,可以扩展一下,为啥这个函数能阻塞当前线程呢?
让我实现肯定第一思路就是使用锁,我们看下它的代码:

里面是一个循环,循环里面使用了一个concurrent包里面的高级API,LockSupport。有兴趣的可以自行查资料看看,反正我当初看了半天没看懂。
现在,我们就可以这样理解,这个函数会使用锁来卡住当前线程,等待协程里面的逻辑完成,然后继续当前线程的执行。
launch

launch 是 CoroutineScope 上面的一个方法,它里面的逻辑是 runBlocking 的简化版,不过协程的类型不一样。
当我们有了一个 CoroutineScope 之后,就可以调用它的 launch 方法来开启一个协程:
@JvmStatic
fun main(args: Array<String>) {
val coroutineScope = CoroutineScope(Dispatchers.IO)
coroutineScope.launch {
}
}
CoroutineScope
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
ContextScope(if (context[Job] != null) context else context + Job())
这里用了一个顶级函数来模拟构造器,真的狗。
我们传递的 context 里面没有 job 的话,这个会默认给我们加一个 job,这一点很重要,因为 job 也是有派系的,会影响到协程里面异常与取消的行为,后面再说。
CoroutineScope 用中文翻译一下,协程域。也就是说它是有生命周期的,肯定会提供取消函数。调用取消函数就会取消协程。
需要注意的是,协程的取消是合作式的,和线程的 Interrupt 一样,如果协程里面直接一个 while(true),这肯定是取消不了的,所以如果你的协程想要相应取消,注意做检查,和使用已有 api。
有兴趣的可以戳一下 viewModelScope 的源码,很简单的。

CoroutineContext
这个玩意就更有意思了,看例子:
val coroutineScope2 = CoroutineScope(CoroutineName("hello"))
coroutineScope2.launch {
}
我们发现,CoroutineName 也是一个 CoroutineContext,这就很神奇。无论我们是传递一个 Dispatchers 还是 Name,都会生成一个 CoroutineContext。就好像里面的逻辑根本不关心你输入的是啥,反正我都能运行。
这是因为协程有一个继承行为,一个协程运行在哪里,需要Dispatchers 决定:
-
当我们没有传递的时候,就会使用继承过来的(父协程)。
-
当我们传递了的时候,就使用自己的
那么,我们的例子中,既没有继承的,我们也没有指定的,会运行在哪里呢?我也不知道,但是我们可以 debug 一下:

可以看到,context 里面给我们加了一个 dispatch,那么就是说会运行在Default线程,也就是子线程。
看下代码是哪里加的:
public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
val combined = foldCopies(coroutineContext, context, true)
val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
debug + Dispatchers.Default else debug
}
是在 return 的时候加的。
可以看到,它会判断如果旧的 context 里面有Dispatchers(也叫 ContinuationInterceptor),那么就不添加,如果没有,那么就添加一个 Dispatchers.Default。
我们的主线程是有 ContinuationInterceptor 属性的,它是一个事件循环。所以,看如下代码:
@JvmStatic
fun main(args: Array<String>) {
runBlocking {
println("${Thread.currentThread().name}")
val coroutineScope2 = CoroutineScope(CoroutineName("hello"))
coroutineScope2.launch {
println("${Thread.currentThread().name}")
}
this.launch {
println("${Thread.currentThread().name}")
}
}
}
下面的 launch 是运行在主线程的。
继承的 context 里面除了有 Dispatchers,还可以有 Job 等。这个就后面再说了。
withContext
虽然这个函数比较复杂,但是有了上面的知识,理解起来也不难。
首先,这个货是一个挂起函数,它只能在协程(宿主)里面调用,协程宿主本身也是有一个 context 的,这个函数也要求传递一个 context,所以会涉及到 context 合并的问题。
context 合并就比较蛋疼了,它相当于里面有两部分:
第一部分是 CopyableThreadContextElement,它是由一个左偏list储存着的,嗯,就理解成一个树就行。可以看下 CombinedContext 类。
第二部分是非 CopyableThreadContextElement 内容,这里面是一个 set,但是是一个支持索引的 set,我们直接理解成一个 set。
像我们的 ContinuationInterceptor 属性就是储存在这个里面的,所以如果子协程设置了 Dispatchers,那么肯定会覆盖父协程的。

看代码实现,总体与上面的两个方法都是差不多的。不过它里面特意加了两个分支逻辑,就是发现线程不需要切换的时候,会直接在当前线程运行,看例子:
@JvmStatic
fun main(args: Array<String>) {
val coroutineScope = CoroutineScope(Dispatchers.IO)
coroutineScope.launch {
println("parent -> ${Thread.currentThread().name}")
withContext(Dispatchers.IO) {
println("child -> ${Thread.currentThread().name}")
}
}
while (true);
}
打印发现,都是在同一个线程,debug一下,确定是走到了 if 分支:



因为新旧 context 对应的 ContinuationInterceptor 属性都是 Dispatchers.IO,所以不用切换线程。
文章详细解释了协程中的runBlocking函数,CoroutineContext的作用,以及如何通过CoroutineScope启动协程。重点讨论了runBlocking如何通过LockSupport实现线程阻塞,以及CoroutineScope与Dispatchers、CoroutineName的关系。
8万+

被折叠的 条评论
为什么被折叠?



