CoroutineScope:
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
定义新协程的范围。每个协程构建器都是CoroutineScope的扩展,并继承其coroutineContext以自动传播上下文元素和取消。
获取范围的独立实例的最佳方法是CoroutineScope()和MainScope()工厂函数。可以使用plus运算符将其他上下文元素附加到作用域。
建议不要手动实现此接口,应优先考虑通过委派实现。按照惯例,作用域的上下文应包含作业实例以强制执行结构化并发。
每个协同程序构建器(如launch,async等)和每个作用域函数(如coroutineScope,withContext等)都会将自己的作用域实例提供给它运行的内部代码块。按照惯例,它们都会等待块内的所有协同程序在完成自己之前完成,从而强制执行结构化并发规则。
CoroutineScope应该在具有明确定义的生命周期的实体上实现(或用作字段),这些实体负责启动子协同程序
CoroutineScope
是必须的么?其实不是的。当协程还是实验性质的时候Kotlin 1.1时,我们启动协程是可以这样写的:
fun requestSomeData() {
launch {
updateUI(performRequest())
}
}
这里我们在UI上下文中启动一个新的协同程序launch(UI),调用挂起函数performRequest
对后端进行异步调用而不阻塞主UI线程,然后用结果更新UI。每个requestSomeData
调用创建自己的协程,它很好,不是吗?
但这是一个问题。如果网络或后端出现问题,这些异步操作可能需要很长时间才能完成。而且,这些操作通常在某些UI元素(如窗口或页面)的范围内执行。如果操作需要很长时间才能完成,则典型用户会关闭相应的UI元素并执行其他操作,或者更糟糕的是,重新打开此UI并一次又一次地尝试操作。但是我们之前的操作仍然在后台运行,当用户关闭相应的UI元素时,我们需要一些机制来取消它。
一个简单的launch { … }
易于编写,但它不是你应该写的。
协同程序始终与应用程序中的某些本地作用域相关,这是一个生命周期有限的实体,如UI元素。因此,对于结构化并发,我们现在要求在CoroutineScope
中调用启动,CoroutineScope
是由您的终身受限对象(如UI元素或其对应的视图模型)实现的接口。
对于更新UI操作CoroutineScope
提供专门的实现,在这里可以看到
对于那些需要全局协程,其生命周期受应用程序生命周期限制的极少数情况,我们现在提供GlobalScope
对象,因此之前为全局协程启动launch{...}
,现在变为GlobalScope.launch {...}
,这个协同程序的全局特性在代码中变得明确。GlobalScope
在之前的几章中经常用到的。
emmm............加入CoroutineScope
就只是解决了这个异步操作的问题么?
再看下面示例:
suspend fun loadAndCombine(name1: String, name2: String): Image {
val deferred1 = async { loadImage(name1) }
val deferred2 = async { loadImage(name2) }
return combineImages(deferred1.await(), deferred2.await())
}
这个例子看起来不错,这个suspend函数
最终会在某个协程内部调用,异步下载2张图片然后合并成一张,但是还是有很多微妙的错误,如果这个协程取消怎么办?然后加载两个图片的异步任务仍然没有受到影响,这不是一个可靠的代码。
那在父协程取消的时候把子协程都取消不就可以了,改成这样async(coroutineContext) { … }
。
它仍然还是有问题,比如下载第一张图片失败了,则deferred1.await()抛出了相应的异常,但是加载第二张图片的协程仍然在后台工作,解决这个问题就更加复杂了。
一个简单async { … }
易于编写,但它不是你应该写的。
使用结构化并发async协同程序构建器CoroutineScope
就像是一样成为扩展launch。你不能简单地写async { … }
,你必须提供范围。一个适当的并行分解的例子变成:
suspend fun loadAndCombine(name1: String, name2: String): Image =
coroutineScope {
val deferred1 = async { loadImage(name1) }
val deferred2 = async { loadImage(name2) }
combineImages(deferred1.await(), deferred2.await())
}
你必须将代码包装到coroutineScope { … }
块中,以建立操作的边界及其范围。所有async
协同程序都成为此范围的子代,如果范围因异常而失败或被取消,则所有子代也将被取消。
协程的团队在引入了结构化并发(Structured concurrency)之后,他们就改变了协程构建器功能launch()
和async()
从顶级更改为使用CoroutineScope接收器的扩展。
coroutineScope方法
为了更加理解coroutineScope,看下下面示例:
@Test
fun main() {
runBlocking {
try {
coroutineScope {
launch { // “1”
println("a")
}
launch {// “2”
println("b")
launch {// “3”
delay(1000)
println("c")
throw ArithmeticException("Hey!!")
}
}
val job = launch {// “4”
println("d")
delay(2000)
println("e")
}
job.join()
println("f")
}
} catch (e: Exception) {
println("g")
}
println("h")
}
}
输出结果:
a
b
d
c
g
h
会发现e,f没有输出。
原因:coroutineScope
是继承外部 Job 的上下文创建作用域,在其内部的取消操作是双向传播的,子协程未捕获的异常也会向上传递给父协程。它更适合一系列对等的协程并发的完成一项工作,任何一个子协程异常退出,那么整体都将退出,简单来说就是”一损俱损“。这也是协程内部再启动子协程的默认作用域。
coroutineSocpe
启动了3个协程,“2”协程又启动了子协程“3”,子协程“3”因为抛出异常取消了。因为coroutineSocpe
异常时双向的所以“3”会通知其父协程“2”取消,2会根据其作用域通知coroutineSocpe
取消,这是一个自下而上的过程,coroutineSocpe取消会通知“4”取消,这是一个自上而下的过程。
其中join()
和delay()
是支持取消的,所以这两处就被取消了e,f就没有被打出来了。
这里有一个小细节我们可以对coroutineSocpe
内部协程中的异常直接try...catch...捕获掉表明协程把异步的异常处理到同步代码逻辑当中。
supervisorScope
再说一个和coroutineSocpe
类似的supervisorScope
:
@Test
fun main() {
runBlocking {
try {
supervisorScope{
launch { // “1”
println("a")
}
launch {// “2”
println("b")
launch {// “3”
delay(1000)
println("c")
throw ArithmeticException("Hey!!")
}
}
val job = launch {// “4”
println("d")
delay(2000)
println("e")
}
job.join()
println("f")
}
} catch (e: Exception) {
println("g")
}
println("h")
}
}
输出:
a
b
d
c
Exception in thread "main @coroutine#5" java.lang.ArithmeticException: Hey!!
...
e
f
h
会发现g没有输出。
原因:supervisorScope
同样继承外部作用域的上下文,但其内部的取消操作是单向传播的,父协程向子协程传播,反过来则不然,这意味着子协程出了异常并不会影响父协程以及其他兄弟协程。它更适合一些独立不相干的任务,任何一个任务出问题,并不会影响其他任务的工作,简单来说就是”自作自受“,例如 UI,我点击一个按钮出了异常,其实并不会影响手机状态栏的刷新。需要注意的是,supervisorScope
内部启动的子协程内部再启动子协程,如无明确指出,则遵守默认作用域规则,也即 supervisorScope
只作用域其直接子协程。
supervisorScope
启动了3个协程,“2”协程又启动了子协程“3”,子协程“3”因为抛出异常取消了。但是因为supervisorScope
的取消操作是单向的即父协程向子协程传播的,所以“3”协程并不会影响“2”协程
@Test
fun main() {
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
println("${coroutineContext[CoroutineName]} $throwable")
}
runBlocking {
try {
supervisorScope {
launch {
// "1"
println("a")
}
launch(exceptionHandler + CoroutineName("\"2\"")) {
// "2"
println("b")
launch(exceptionHandler + CoroutineName("\"3\"")) {
//"3"
launch (exceptionHandler + CoroutineName("\"5\"")){// "5"
delay(1000)
println("c-")
}
println("c")
throw ArithmeticException("Hey!!")
}
}
val job = launch {
//"4"
println("d")
delay(2000)
println("e")
}
job.join()
println("f")
}
} catch (e: Exception) {
println("g")
}
println("h")
}
}
仔细看下输出:
a
b
d
c
CoroutineName("2") java.lang.ArithmeticException: Hey!!
e
f
h
异常竟然是协程“2”打出来的而且c-和g没有打出来。
其实并不意外,supervisorScope
内部启动的子协程内部再启动子协程,如无明确指出,则遵守默认作用域规则,也即 supervisorScope
只作用于其直接子协程。默认作用域规则就是coroutineScope
,子协程未捕获的异常也会向上传递给父协程。
GlobeScope
看一个示例:
fun work(i: Int) {
Thread.sleep(1000)
println("Work $i done")
}
@Test
fun main() {
val time = measureTimeMillis {
runBlocking {
for (i in 1..2) {
launch {
work(i)
}
}
}
}
println("Done in $time ms")
}
输出的结果:
Work 1 done
Work 2 done
Done in 2095 ms
它打印Work 1 done和Work 2 done,但它需要两秒钟才能完成。并发在哪里?launch已经继承了从引进范围协程调度runBlocking
协同程序生成器,该组合限制住执行到单个线程,所以这两个任务在主线程中执行顺序。
要并发换成这样就行了:
launch(Dispatchers.Default) {
work(i)
}
这样就能在1s中完成了。
如果我换成GlobalScope
启动协同程序会发生什么?它应该是相同的,因为它在后台线程Dispatchers.Default
中执行协程。
@Test
fun main() {
val time = measureTimeMillis {
runBlocking {
for (i in 1..2) {
GlobalScope.launch {
work(i)
}
}
}
}
println("Done in $time ms")
}
输出结果:
Done in 97 ms
并没有打印Work x done,直接打印了Done in 97 ms。为什么?
原因:通过 GlobeScope 启动的协程单独启动一个协程作用域,内部的子协程遵从默认的作用域规则。通过 GlobeScope 启动的协程“自成一派”。
GlobeScope.launch{...}
和launch(Dispatchers.Default){...}
的区别就出来了。启动(Dispatchers.Default)
在runBlocking
范围内创建子协程,因此runBlocking
会自动等待它们的完成。但是,GlobalScope.launch
创建了全局协程。
我们可以通过以下手段控制来达到和launch(Dispatchers.Default){...}
同样的效果:
@Test
fun main() {
val time = measureTimeMillis {
runBlocking {
val jobs = mutableListOf<Job>()
for (i in 1..2) {
jobs += GlobalScope.launch {
work(i)
}
}
jobs.forEach { it.join() }
}
}
println("Done in $time ms")
}
现在输出:
Work 1 done
Work 2 done
Done in 1102 ms
现在这个例子与GlobalScope
代码的工作方式类似launch(Dispatchers.Default)
,但需要付出更多努力,为什么还要编写更多代码?几乎没有理由GlobalScope
在基于Kotlin协同程序的应用程序中使用。
对于上面的操作还可以这样:
suspend fun work(i: Int) = withContext(Dispatchers.Default) {
Thread.sleep(1000)
println("Work $i done")
}
tips:
对于没有协程作用域,但需要启动协程的时候,适合用
GlobalScope
对于已经有协程作用域的情况(例如通过
GlobalScope
启动的协程体内),直接用协程启动器启动对于明确要求子协程之间相互独立不干扰时,使用
supervisorScope
对于通过标准库 API 创建的协程,这样的协程比较底层,没有 Job、作用域等概念的支撑,例如我们前面提到过 suspend main 就是这种情况,对于这种情况优先考虑通过
coroutineScope
创建作用域;更进一步,大家尽量不要直接使用标准库 API,除非你对 Kotlin 的协程机制非常熟悉。
launch
fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
// ...
): Job
它被定义为CoroutineScope
上的扩展函数,并将CoroutineContext
作为参数,因此它实际上需要两个协程上下文(因为范围只是对上下文的引用)。
它与它们有什么关系?它使用plus运算符合并它们,生成其元素的集合,以便context参数中的元素优先于作用域中的元素。生成的上下文用于启动新的协程,但它不是新协程的上下文而是新协程的父上下文。新的协程创建自己的子Job实例(使用此上下文中的job作为其父)并将其子上下文定义为父上下文plus其job:
图片来自于:Coroutine Context and Scope
a,按照惯例,CoroutineScope
中的上下文包含一个Job
,它将成为新的coroutine的父级
(GlobalScope除外,你应该避免)。
b,启动时的CoroutineContext
参数是提供额外的上下文元素来覆盖否则将从父作用域继承的元素。
c,按照惯例,我们通常不会在上下文参数中传递Job来启动,因为这会破坏父子关系,除非我们明确想要使用NonCancellable
作业来打破它。
d,按照惯例,所有协程构建器作用域的coroutineContext
属性与在此block内运行的协同程序的上下文相同。
@Test
fun main() = runBlocking<Unit> {
launch { scopeCheck(this) }
}
suspend fun scopeCheck(scope: CoroutineScope) {
println(scope.coroutineContext === coroutineContext)
}
输出为:true
e,由于上下文和范围在本质上是相同的,我们可以在没有访问范围的情况下启动协程,而不使用GlobalScope
只需将当前coroutineContext
包装到CoroutineScope
的实例中,如以下函数所示:
suspend fun doNotDoThis() {
CoroutineScope(coroutineContext).launch {
println("I'm confused")
}
}
不要这样做!它使协程的启动范围变得不透明和隐含,捕获一些外部Job来启动一个新的协程,而不在函数签名中明确地宣布它。协程是与您的其余代码同时进行的一项工作,其启动必须是明确的.