Kotlin协程教程2协程作用域与各种builder们

7次阅读

共计 2403 个字符,预计需要花费 7 分钟才能阅读完成。

作用域与上下文

协程作用域本质是一个接口,既然是一个接口,那么它就可以被某个类去实现(implement),实现它的那个类,也就具备了一些能力。

class MyClass: CoroutineScope {// MyClass 就具备了 CoroutineScope 的一些能力}

那么它具备了哪些能力呢?

当然是启动协程的能力和停止协程的能力。除了 runBlocking 有一些特殊外,launch 和 async 其实都是 CoroutineScope 的扩展方法,它们两个都必须通过作用域才能调用。

比如我们有一个界面,里面有一些数据是需要通过网络或者文件或者数据库才能获取的,我们想通过协程去获取它们,但由于界面可能随时会被关闭,我们希望界面关闭的时候,协程就不要再去工作了。

我们可以这样写

class MyClass: CoroutineScope by CoroutineScope(Dispatchers.Default) {fun doWork() {
        launch {for (i in 0..10) {println("MyClass launch1 $i -----")
                delay(100)
            }
        }
    }

    fun destroy() {(this as CoroutineScope).cancel()}
}

fun main() {val myClass = MyClass()

    // 因为 myClass 已经是一个 CoroutineScope 对象了,当然也可以通过这种方式来启动协程
    myClass.launch {for (i in 0..10) {println("MyClass launch1 $i *****")
            delay(100)
        }
    }

    myClass.doWork()

    Thread.sleep(500) // 让协程工作一会

    myClass.destroy() // myClass 需要被回收了!Thread.sleep(500) // 等一会方便观察输出
}

当 destroy 被调用的时候,myClass 的协程就都停止工作了,是不是很爽,很方便。这个设计将非常适合与在 GUI 程序上使用。

现在来小小的回顾下上面说的,协程必须要在 CoroutineScope 中才能启动,本质是 launch 和 async 是 CoroutineScope 的扩展方法,在一个协程作用域 CoroutineScope 中启动的协程,都将受到这个作用域的管控,可以通过这个作用域的对象来取消内部的所有协程。

协程作用域 CoroutineScope 的内部,又包含了一个 协程上下文(CoroutineContext) 对象。

协程上下文对象中,是一个 key-value 的集合,其中,最重要的一个元素就是 Job,它表示了当前上下文对应的协程执行单元。

它们的关系看起来就像是这样的:

另外,launch 和 async 启动后的协程,也是一个新的作用域,如下代码,我构造了好几个协程,并 print 出当前的 Scope 对象。

GlobalScope.launch {println("GlobalScope ${this.toString()}")
    launch {println("A ${this.toString()}")
        launch {println("A1 ${this.toString()}")
        }
    }

    launch {println("B ${this.toString()}")
    }
}

运行结果:

GlobalScope StandaloneCoroutine{Active}@714834a4
B StandaloneCoroutine{Active}@6be16ee2
A StandaloneCoroutine{Active}@6a716a81
A1 StandaloneCoroutine{Active}@64b699bf

可见,作用域启动新协程也是一个新的作用域,它们的关系可以并列,也可以包含,组成了一个作用域的树形结构。

默认情况下,每个协程都要等待它的子协程全部完成后,才能结束自己。这种形式,就被称为 结构化的并发

各种 builder 们

关于 GlobalScope.launch,还有一个小特性,就是它更像一个守护线程,无法使进程保活。具体来说,就是,如果进程中只有这样一个守护线程,可能会被干掉。

fun main()  {
    runBlocking {
        launch {for (i in 0 ..100) {delay(1000)
                println("hahaha")
            }
        }
    }
    
    println("qqqqqqqqqqqq")
}

上面的代码,可以正确的输出一堆 hahahah,最终会输出 qqqqqqqq。

但如果将 launch 换成 GlobalScope.launch,就是另一种效果了

fun main()  {
    runBlocking {
        GlobalScope.launch { // 注意这里的变化
            for (i in 0 ..100) {delay(1000)
                println("hahaha")
            }
        }
    }

    println("qqqqqqqqqqqq")
}

运行结果为:
直接输出 qqqqqqqqqqqqq,进程就结束了。

在官方文档上,launch、async 被称为 coroutine builder,我想不严谨的扩大一下这个概念,将经常使用到的都成为 builder,我已经总结了它们的特性,列在下面的表格中:

总结

协程作用域本质是一个接口,我们可以手动声明这样一个接口,也可以让一个类实现这个接口。在语义上,仿佛就像定义了一个作用域,但又巧妙的在这个作用域的范围内,可以使用启动协程的方法了,启动的协程也自然的绑定在这个作用域上。

新启动的协程又会创建自己的作用域,可以自由的组合和包含,外层的协程必须要等到内部的协程全部完成了,才能完成自己的,这便是结构化的并发。

协程作用域实际上是绑定了一个 Job 对象,这个 Job 对象表示作用域内所有协程的执行单元,可以通过这个 Job 对象取消内部的协程。

如果你喜欢这篇文章,欢迎点赞评论打赏
更多干货内容,欢迎关注我的公众号:好奇码农君

正文完
 0