![](https://img-blog.csdnimg.cn/2392af7e026f4b118a5bfc0a9df3df8e.png)
![](https://img-blog.csdnimg.cn/0c086a16c85f46db84b0f24c92e30e89.png)
CoroutineScope - 协程作用域
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
-
顶级作用域 没有父协程的协程所在的作用域为顶级作用域。
-
协同作用域 协程中启动新的协程,新协程为所在协程的 子协程,这种情况下, 子协程所在的作用域默认为协同作用域。此时 子协程抛出的未捕获异常,都将传递给父协程处理,父协程同时也会被取消。
-
主从作用域 与协同作用域在协程的父子关系上一致,区别在于, 处于该作用域下的协程出现未捕获的异常时,不会将异常向上传递给父协程。
-
父协程被取消,则所有子协程均被取消。由于协同作用域和主从作用域中都存在父子协程关系,因此此条规则都适用。
-
父协程需要等待子协程执行完毕之后才会最终进入完成状态,不管父协程自身的协程体是否已经执行完。
-
子协程会继承父协程的协程上下文中的元素, 如果自身有相同key的成员,则覆盖对应的key,覆盖的效果仅限自身范围内有效。
![](https://img-blog.csdnimg.cn/2181df725dd84572ab298bd9c05e80da.png)
![](https://img-blog.csdnimg.cn/86278ef577c74eeb90c47bab1ec24582.png)
![](https://img-blog.csdnimg.cn/6d65a421acb94a3494ccef1ba114e8e8.png)
使用协程作用域来创建协程
-
GlobeScope:全局范围,不会自动结束执行。
-
MainScope:主线程的作用域,全局范围
-
lifecycleScope:生命周期范围,用于activity等有生命周期的组件,在Desroyed的时候会自动结束。
-
viewModeScope:ViewModel范围,用于ViewModel中,在ViewModel被回收时会自动结束
如何使用 coroutineScope 启动协程
-
调用 xxxScope.launch{...} 启动一个协程块, launch方法启动的协程不会将结果返回给调用方。任何被视为“一劳永逸”的工作都可以使用 launch来启动。
-
在 xxxScope {...} 中调用 async{...} 创建一个子协程, async会返回一个 Deferred对象,随后可以调用 Deferred对象的 await()方法来启动该协程。
-
withContext(){...} 一个 suspend方法,在给定的上下文中执行并返回结果,它的目的不在于启动子协程,主要用于 线程切换,将长耗时操作从UI线程切走,完事再切回来。用它执行的挂起块中的上下文是当前协程的上下文和由它执行的上下文的合并结果。
-
coroutineScope{...} 一个 suspend方法,创建一个新的作用域,并在该作用域内执行指定代码块,它并不启动协程。其存在的目的是进行符合结构化并发的并行分解。
-
runBlocking{...} 创建一个协程,并阻塞当前线程,直到协程执行完毕。
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
...省略
}
![](https://img-blog.csdnimg.cn/dcab519479fe47bbadc10507b58a0143.png)
coroutineScope & supervisorScope
private fun request() {
lifecycleScope.launch {
coroutineScope { // 协同作用域,抛出未捕获异常时会取消父协程
launch { }
}
supervisorScope { // 主从作用域,抛出未捕获异常时不会取消父协程
launch { }
}
}
}
注意这两个函数的作用只是定义了2个作用域而已,如果想要启动新的子协程请在里面调用launch。如果需要异步请使用async。
-
supervisorScope 表示 主从作用域,会继承父协程的上下文,它的特点就是子协程的异常不会影响父协程,内部的 子协程挂掉 不会影响外部的父协程和兄弟协程的继续运行,它就像一道防火墙,隔离了异常,保证程序健壮,但是如果外部协程挂掉还是可以取消子协程的,即 单向传播。它的设计应用场景多用于 子协程为独立对等的任务实体的时候,比如一个下载器,每一个子协程都是一个下载任务,当一个下载任务异常时,它不应该影响其他的下载任务。
-
coroutineScope 表示 协同作用域, 内部的协程 出现异常 会向外部传播,子协程未捕获的异常会向上传递给父协程, 子协程 可以挂掉外部协程 , 外部协程挂掉也会挂掉子协程,即 双向传播 。 任何一个子协程异常退出,会导致整体的退出。
![](https://img-blog.csdnimg.cn/15d2dc288046484e8522a2bccb89b9f0.png)
还可以进行一些简单的封装,比如我们可以定义一个 suspend 方法,内部返回一个 coroutineScope 作用域对象来执行一个传入的协程代码块:
private suspend fun saveLocal(coroutineBlock: (suspend CoroutineScope.() -> String)? = null): String? {
return coroutineScope {
// 以下几种写法等价,都是执行block代码块
// coroutineBlock!!.invoke(this)
// coroutineBlock?.invoke(this)
// if (coroutineBlock != null) {
// coroutineBlock.invoke(this)
// }
coroutineBlock?.let { block ->
block()
}
}
}
MainScope().launch {
println("执行在一个协程中...")
val result = saveLocal {
async(Dispatchers.IO) {
"123456"
}.await()
}
println("一个协程执行完毕... result:$result")
}
并行分解
![](https://img-blog.csdnimg.cn/142c4a4bf4ac4d91b33252a4572eda57.gif)
例如,假设我们定义一个用于异步获取两个文档的 coroutineScope。通过对每个延迟引用调用 await(),我们可以保证这两项 async 操作在返回值之前完成:
suspend fun fetchTwoDocs() = coroutineScope {
val deferredOne = async { fetchDoc(1) }
val deferredTwo = async { fetchDoc(2) }
deferredOne.await()
deferredTwo.await()
}
![](https://img-blog.csdnimg.cn/88a359b627c14fd1a2269c54b7018c02.png)
假如像上面这样直接使用coroutineScope,那么async执行完成,coroutineScope中排在async之后的代码有可能被调度到某个子线程中执行,即上面的红色部分执行完后,蓝色部分可能运行在某个子线程中。如下图:
所以在Android中,最好是在lifecycleScope或viewModelScope中去使用async, 这样能保证async之后的代码仍然执行在主线程上。但是此时在lifecycleScope或viewModelScope中调用的async中的代码也会执行在主线程(虽然是异步的,但既然是主线程就会有IO太长阻塞主线程的风险),也就是说async默认跟父协程的调度器是一样的,因此,如果有需要,此时可以为async指定线程调度器。如下:
![](https://img-blog.csdnimg.cn/aafb04229b574a858d200fbae211b359.png)
除了单独调用每个await方法,还可以对集合使用 awaitAll(),如以下示例所示:
suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main)
coroutineScope {
val deferreds = listOf( // fetch two docs at the same time
async { fetchDoc(1) }, // async returns a result for the first doc
async { fetchDoc(2) } // async returns a result for the second doc
)
deferreds.awaitAll() // use awaitAll to wait for both network requests
}
虽然 fetchTwoDocs() 使用 async 启动新协程,但该函数使用 awaitAll() 等待启动的协程完成后才会返回结果。 此外,coroutineScope 会捕获协程抛出的所有异常,并将其传送回调用方。
写法上需要注意的点:
suspend fun main() = runBlocking {
val times = measureTimeMillis {
// 这样写是串行执行,总耗时2s
val one = doOne()
val two = doTwo()
println("The result is ${one + two}")