一、概念 

协程作用域 CoroutineScope 是一个接口,没有任何抽象方法需要实现,仅仅维护一个成员变量 CoroutineContext(协程上下文),将作为初始上下文对象传递给被创建的协程,不同的实现类或作用域函数本质上的区别是持有的协程上下文不同(配置不同)。 

实现类核心库GlobalScope
工厂函数

MainScope( )

CoroutineScope( )

平台支持

ViewModel.viewModelScope

LifecycleOwner.lifecycleScope

扩展函数协程构建器

launch( )

async( )

作用域函数普通函数runBlocking( )
挂起函数

coroutineScope( )

supervisorScope( )

withContext( )

withTimeout( )

withTimeoutOrNull( )

二、普通函数 runBlocking()

会阻塞当前线程直到其内部所有协程执行完毕,内部的协程彼此之间依旧是非阻式。用于把阻塞式的普通函数内部改为协程式编写,由于会阻塞线程在开发中不会使用,一般用于main函数作测试,单元测试一般使用runTest。

public fun <T> runBlocking(

        context: CoroutineContext = EmptyCoroutineContext,

        block: suspend CoroutineScope.() -> T

): T

fun main(): Unit = runBlocking {
    launch { println("...") }
}

三、实现类

3.1 核心库

3.1.1 GlobalScope

单例对象,不推荐使用。全局协程作用域,不绑定到任何Job上无法取消,通过它启动的子协程不会阻塞其所在线程可以一直运行到APP停止(相当于守护线程不会阻止JVM结束运行),子协程运行在自己的调度器上不会继承上下文与父协程没有联系,因此所有开启的子协程都需要分别手动来管理(容易造成内存泄漏和CPU冗余使用,例如当Activity销毁后协程还在执行耗时操作占用资源)。

3.1.2 ContextScope

上下文作用域,intermal修饰未对外暴露,根据指定的上下文创建协程作用域。使用工厂函数 MainScope()、CoroutineScope() 传入上下文对象参数,获取到的就是 ContextScope 实例。 

3.2 工厂函数

3.2.1 mainScope( )

默认上下文使用 SupervisorJob()+Dispatchers.Main 的协程作用域。该调度器会绑定到主线程(在Android中就是 UI Thread),在 onDestroy() 中调用 scope.cancel() 关闭协程。可用于主动控制协程的生命周期,对Android开发意义在于避免内存泄漏。

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
class AndroidActivity{
    private val scope = MianScope()
    scope.launch{
        launch{
            coroutineContext    //父协程的上下文
            currentCoroutineContext()    //当前协程的上下文
        }
    }
    override fun onDestroy(){
        scope.cancel()    //手动在生命周期里释放资源
    }
}

3.2.2 CoroutineScope( )

根据自定义上下文创建协程作用域(如果上下文中没有 Job 会自动创建一个用于结构化并发)。CoroutineScope是一个只包含 coroutineContext 属性的接口,虽然我们可以创建一个实现类但这不是一个流行的做法,而且存在不小心在其它地方取消作用域。通常我们会更喜欢通过对象来启动协程,最简单的办法是使用 CoroutineScope() 工厂函数,它用传入的上下文来创建作用域。

public fun CoroutineScope(context: CoroutineContext): CoroutineScope = ContextScope(if (context[Job] != null) context else context + Job())

3.3 Android平台支持

3.3.1 ViewModel.viewModelScope( )

是ViewModelKTX 提供的扩展属性。使用的上下文是SupervisorJob() + Dispatchers.Main.immediate,ViewModel销毁时协程作用域会自动被 cancel,避免造成协程泄漏(内存泄漏)。

public val ViewModel.viewModelScope: CoroutineScope

3.3.2  LifecycleOwner.lifecycleScope( )

是 LifeCycleKTX 提供的扩展属性。使用的上下文是SupervisorJob() + Dispatchers.Main.immediate,在 Activity/Fragment 销毁时协程作用域会自动被 cancel,避免造成协程泄漏(内存泄漏)。

public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope

四、扩展函数

4.1 协程构建器 CoroutineBuilder

        launch() 和 async() 是 CoroutineScope 接口的扩展函数,继承了它的 coutineContext 来自动传播其上下文元素和可取消性。挂起函数需要相互传递 Continuation,每个挂起函数都要由另一个挂起函数或协程调用,这一切都是从协程构建器创建协程开始的,即作用域函数只能创建子协程,协程构建器能创建根协程或子协程(因为它通过实例调用可以存在于普通函数中)。

        挂起函数创建的子协程是串行运行,协程构建器创建的子协程是并行运行。

参数 context:指定协程上下文。默认为空的上下文。

参数 start:指定协程启动模式。默认为可以立刻被调度的状态。

参数 block:协程执行体,即要做的任务。

launch( )

无需产生值

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

返回一个Job实例用来管理协程的生命周期。

async( )

需要产生值

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> 

返回一个Deferred实例(Job的子类),通过 await() 拿到执行的结果(包括异常)。由于 await() 是挂起函数只能在协程作用域中调用,因此不要用 async() 做根协程,拿不到值就相当于 launch()。通常用于在协程作用域中构建并发子协程合并结果。

4.1.1 协程启动模式 CoroutineStart

CoroutineStart.DEFAULT协程创建后立即开始调度(不一定此时就被线程执行了),在被线程执行前如果协程被取消,其将直接进入取消响应状态,
CoroutineStart.LAZY只要协程被需要时(包括主动调用 start()、join()、await())才会开始调度。如果调度前被取消,协程将进入异常结束状态。
CoroutineStart.ATOMIC协程创建后立即开始调度,内部代码执行到第一个挂起点之前不响应取消操作(内部第一个挂起函数之前的代码一定执行)。
CoroutineStart.UNDISPATCHED协程被创建后立即在当前函数调用栈中执行(所处的函数在哪个线程就是哪个,即便该协程通过Dispatcher指定了运行的线程),直到内部代码执行到第一个挂起点,挂起函数运行完后,之后的代码就是在Dispatcher指定的线程中运行了。
//ATOMIC模式
val job = launch(start = CoroutineStart.ATOMIC) {
    //这里的代码一定会执行
    delay(10000)   //第一个挂起点
    println("Job 完成")
}
delay(1000)
job.cancel()   //取消上面的job
println("main 结束")

//LAZY模式
val deferred = async(start = CoroutineStart.LAZY) {
    27
}
//此处执行一些计算...之后才需要拿到 deferred的值
deferred.await()    //如果在这之前调用cancel()取消,就直接抛异常JobCancellationException

//UNDISPATCHED模式
launch(context = Dispatchers.IO, start = CoroutineStart.UNDISPATCHED) {
    println("挂起之前:" + Thread.currentThread().name)  //打印:main
    delay(10)
    println("挂起之后:" + Thread.currentThread().name)  //打印:DefaultDispatcher-worker-1
}

五、作用域函数

都是挂起函数不会阻塞线程。由于挂起需要协程环境,只能由其它挂起函数或构建器调用,因此只能用来创建子协程。形参上将Lambda的接收者指定为CoroutineScope,因此可以在内部调用构建器开启子协程,通常被用于包装函数(一个作用域包装一堆调用尤其是withContext() )。

异常结束自身supervisorScope( )
异常连锁反应指定上下文withContext( )
继承上下文coroutineScope( )
限制执行时间

withTimeout( )

withTimeoutOrNull( )

注意:

  1. 无法使用withContext(SupervisorJob())替代supervisorScope,因为withContext仍会使用常规Job,SupervisorJob只是它的父级Job。
  2. 需要同时使用多个协程作用域函数的功能,需要嵌套使用。
suspend fun main() = runBlocking {
    val scope = CoroutineScope(Dispatchers.Default)
    scope.launch {
        delay(1000)
        println("协程1")
    }
    scope.launch {
        delay(1000)
        println("协程2")
    }
//  scope.cancel()    //作用域被取消,里面的子协程都会被取消
    //delay是因为scope作用域自定义了上下文(调度器),没有继承父协程runBlicking的。
    //它指定在了其它线程执行,自己也没有挂起特性,主线程会发现没有任务了会直接结束。
    //也说明了协程不会阻塞线程。
    delay(2000)
}

5.1 coroutineScope( )  

  • 异常:会连锁取消子协程、兄弟协程和父协程。
  • 上下文:继承上下文
  • 使用场景:经常被用来包装一个挂起函数的主体。多用于并行分解任务逻辑。
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R
suspend fun main() = runBlocking {    //将main()函数内部改为支持协程式编写,内部全部子协程运行完才会退出
    //使用coroutineScope会挂起父协程runBlocking,不然因为协程不会阻塞线程,runBlocking会直接结束
    coroutineScope { //这里换成supervisorScope,子协程1就会执行
        val job1 = launch {
            delay(5000)
            println("子协程1")
        }
        val job2 = launch {
            delay(1000)
            println("子协程2")
            throw Exception()   //子协程1不会执行
        }
    }
}

5.2 supervisorScope( ) 

  • 异常:不会影响兄弟协程和父协程,相当于一个独立的根协程,只关注自己内部。
  • 上下文:继承上下文
  • 使用场景:主要用于启动多个独立任务。
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R

5.3 withContext( ) 

  • 异常:会连锁取消子协程、兄弟协程和父协程。
  • 上下文:指定上下文
  • 使用场景:经常用来指定协程执行的线程和启动模式。这样在封装函数的时候,里面的业务代码就会被执行在正确的线程中(如 CPU密集型计算、IO、更新UI)
public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T
suspend fun getSum(num1: Int, num2: Int): Int = withContext(Despatcher.DEFAULT) {
    num1 + num2
}

suspend fun getData(): UserBean = withContext(Despatcher.IO) {
    Retrofit.apiService.getData()
}

5.4 withTimeout( ) 

  • 异常:会连锁取消子协程、兄弟协程和父协程。
  • 上下文:继承上下文
  • 使用场景:超时未执行完会抛异常,并返回一个值。超时抛出的TimeoutCancellationException是CancellationException子类,因此不会影响其他协程。

public suspend fun <T> withTimeout(

        timeout: Duration,

        block: suspend CoroutineScope.() -> T

): T

withTimeout(1000) {
    println("")
}

5.5 withTimeoutOrNull( ) 

  • 异常:子协程异常会连锁取消其它子协程和自己。
  • 上下文:继承上下文
  • 使用场景:超时未执行完不抛异常,返回null。用来包装那些出现异常后会一直等待的操作,例如网络操作等待结果超过5s后不太可能会收到结果了。

public suspend fun <T> withTimeoutOrNull(

        timeMillis: Long,

        block: suspend CoroutineScope.() -> T

): T? 

withTimeoutOrNull(1000) {
    println("")
} ?: "值为空"

六、创建协程的区别

协程构建器和协程作用域函数中都包含了形参 block: suspend CoroutineScope.() -> Unit。在这个block里写代码就相当于在协程作用域的一个函数里写代码,因此我们可以通过属性coroutineContext拿到协程上下文,能调用协程构建器去创建子协程,也能调用挂起函数去挂起当前协程。

全局GlobalScope
阻塞runBlocking( )
生命感知

MainScope( )

ViewModel.viewModelScope

LifecycleOwner.lifecycleScope

自定义CoroutineScope( )
挂起异常结束自身supervisorScope( )
异常连锁反应指定上下文withContext( )
继承上下文coroutineScope( )
限制执行时间

withTimeout( )

withTimeoutOrNull( )

通过 协程构建器 创建通过 协程作用域函数 创建

①通过协程作用域实例调用,创建的可能是根协程(在普通函数中)也可能是子协程(在其它协程中);

②直接调用创建的只能是子协程(必须存在父协程,即外部有上面说的block包裹)。

创建的只能是子协程。挂起函数“挂起恢复”的特性只能在协程环境下实现,因此只能在其它挂起函数或协程中调用,一定存在父协程。

携带来自CoroutineScope的协程上下文

异常通过Job传递给父协程

构建器彼此之间是并发的

携带contineation的协程上下文

异常像普通函数那样抛出

挂起函数彼此之间是阻塞的

tihs是被创建出来的协程,因为调用block的是被创建出来的协程,协程都是 AbstractCoroutine 的子类,而它实现了CoroutineScope,并不是调用它的协程作用域对象。

因此在block中创建子协程:

①通过协程构建器:能直接通过 coroutineScope 属性拿到当前协程的上下文继承,从而形成父子关系具备传播取消和异常。

②通过作用域函数:父协程被挂起时会返回一个 Continuation 延续体给挂起函数,这个延续体就是父协程(协程都是 AbstractCoroutine 的子类,而它实现了Continuation),从而获取并继承上下文,形成父子关系。

③通过协程作用域对象调用协程构建器:不是子协程,因为没有上下文继承关系,无法通过Job传播取消和异常。GlobalScope是EmptyCoroutineContext,而MainScop、ViewModelScope、LifecyleScope都是指定的SupervisorJob(),至于CoroutineScope()虽然构造可以传入父协程上下文构建内外相同的作用域,何必多此一举。

GlobalScope.launch {
    //不是子协程,外层的取消不会取消它
    CoroutineScope(Dispatchers.IO).launch { }
    //子协程,虽然创建了新的作用域对象,但是通过coroutineContext获取了父协程上下文,间接继承
    CoroutineScope(coroutineContext).launch { }
    //不是子协程,Job会替换掉父协程中的Job
    CoroutineScope(coroutineContext + Job()).launch { }
}
多任务-串行launch + 多个 withContext
多任务-并行launch + 多个 async
launch + 多个 launch

七、多个协程作用域之间的关系

在处理过程中,需要执行一个额外的非必要操作(例如收集数据分析),若是存在于挂起函数中,则会额外等待这个操作执行完,若这个操作发生异常还会波及正常的业务,这个时候最好是放在一个单独的作用域里启动它。一般通过构造(单元测试、控制此作用域)或者函数注入,如果是为了调用一些函数使用SupervisorJob,如果是收集异常信息使用CoroutineExceptionHandler。

在结构化并发中,由于作用域存在嵌套使用,因此有多种情况:

类型场景举例异常传播特征
顶级作用域1.根协程之间。2.GlobalScope嵌套GlobalScope彼此独立互不影响。3.A和B是两个作用域对象,A开启的作用域中B开启了作用域,两个作用域彼此独立互不影响。4.supervisorScope() 或 supervisorJob 由于使用了新的 Job,相当于是一个独立的根协程,与外部互不影响。不向外传播。
协同作用域外层有父协程,且自身非另外的作用域对象开启。双向传播。
主从作用域外层有父协程,自身是supervisorScope()或supervisorJob。与内部直接子协程主从,与外部协同。向下单向传播。

八、其它函数/属性

8.1 cancel( )

函数一:public fun CoroutineScope.cancel(cause: CancellationException? = null) 

函数二:public fun CoroutineScope.cancel(message: String, cause: Throwable? = null): Unit = cancel(CancellationException(message, cause))

取消协程会抛异常,默认可空也可以自定义scope.cancel(CancellationException("取消")),作用域里的子协程会全部被取消。底层调用的是Job的取消。作用域的上下文中没有检测出包含Job会抛异常(例如GlobalScope的上下文是EmptyCoroutineContext就无法使用,但是它调用launch()后返回的Job可以使用)。一般用于有生命周期的组件中释放资源避免内存泄漏。

九、Android开发

直接使用 viewModelScope() 和 lifecycleScope(),以下是手写的情况。

  • 定义在基类中:通常会在 ViewModel 中启动协程,在其它层例如仓库只创建挂起函数。
  • 上下文:
    • 调度器:Android中默认为主线程,因此选择 Dispatcher.Main.immediate。
    • Job:不能因为子协程异常而连带取消其它协程,例如用户信息加载出错影响动态列表功能,因此选择 SupervisorJob。
  • 生命周期:
    • 方式一:在 ViewModel 的 onCleared() 中关闭协程作用域。
    • 方式二:不取消整个作用域而是取消子协程,因此只要 ViewModel 存活,就可以继续启动新的协程。
  • 异常:
    • 方式一:在 BaseActivity 中写下异常处理方法,通过构造传递给 ViewModel。有未处理的异常就可以用 CoroutineExceptionHandler 里调用函数。
    • 方式二:使用 Livedata 保存。有未处理的异常就可以在 CoroutineExceptionHandler 里对 LiveData 赋值,在其它地方观察处理。
abstract class BaseViewModel(
    private val onError: (Throwable) -> Unit
) : ViewModel() {
    private val context = Dispatchers.Main.immediate + SupervisorJob() + exceptionHandler
    protected val scope = CoroutineScope(context)
    private val failure: MutableLiveData<Throwable> = MutableLiveData()
    private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
            onError(throwable)
            failure.value = throwable
        }
    
    override fun onCleared() {
        //scope.coroutineContext.cancelChildren()
        context.cancelChildren()
    }
}
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐