协程02 - 基本API介绍

文章详细解释了协程中的runBlocking函数,CoroutineContext的作用,以及如何通过CoroutineScope启动协程。重点讨论了runBlocking如何通过LockSupport实现线程阻塞,以及CoroutineScope与Dispatchers、CoroutineName的关系。

之前写了一篇协程速通,发现在阅读协程代码的时候还是很会卡住,细想了一下,发现是很多协程的招式看不懂。所以,非常有必要学习并理解一下基本的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,所以不用切换线程。

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二手的程序员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值