Kotlin 协程 - 协程作用域 CoroutineScope

本文详细探讨了协程的CoroutineScope、CoroutineBuilder和协程作用域函数的使用,包括runBlocking、GlobalScope、ContextScope、viewModelScope与lifecycleScope的区别,以及如何创建根协程和子协程,以及协程上下文、异常传播和生命周期管理的最佳实践。

一、概念 

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

实现类(对象):提供环境

核心库GlobalScope
工厂函数

MainScope( )

CoroutineScope( )

平台支持

ViewModel.viewModelScope

LifecycleOwner.lifecycleScope

扩展函数:创建任务

协程构建器

launch( )

async( )

作用域函数:业务代码

普通函数runBlocking( )
挂起函数

coroutineScope( )

supervisorScope( )

withContext( )

runInterruptible( )

withTimeout( )

withTimeoutOrNull( )

二、普通函数 runBlocking()

用于把阻塞式的普通函数改造成协程环境,会阻塞当前线程直到其内部所有协程执行完毕。由于会阻塞线程在开发中不会使用,一般用于改造main函数用来测试代码,单元测试一般使用runTest。

public actual fun <T> runBlocking(
        context: CoroutineContext,        //不指定Dispatchers的话,默认是BlockingEventLoop,不是Default
        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平台支持

一般情况下,把所有耗时操作都定义为挂起函数,最终都是在 UI 或 ViewModel 中调用,不太会需要用到自定义的应用作用域。这样当生命周期结束的时候,可以取消所有任务避免内存泄漏。

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

3.3.3 自定义应用作用域

        不推荐,因为业务被调用的入口就两个位置,UI 和 ViewModel 中,其他地方不应该有作用域,不受调用者控制违背了结构化并发。

        相比于全局作用域 GlobalScope,应用作用域 AppScope 可以在需要时取消所有协程,可用于处理比单个屏幕声明周期更长的操作。

class App : Application() {

    private val exceptionHandler = CoroutineExceptionHandler {context, throwable -> 
        //异常处理
    }
    companion object {
        lateinit var appScope: CoroutineScope
            private set
    }

    override fun onCreate() {
        super.onCreate()
        appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + exceptionHandler)
    }
    
    override fun onTerminate() {
        super.onTerminate()
        appScope.cancel() // 清理取消
    }
}

四、扩展函数

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()

runInterruptible()

限制执行时间

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() 

  • 异常:不会影响兄弟协程和父协程。
  • 上下文:使用 supervisorJob 而不是基于父协程构建,自定义 Job 意味着失去结构化并发(父协程的取消不会连带取消该子协程,该子协程的异常不会向上传播)。它是一个独立的作用域,是根协程,开启的子协程是顶级协程,需要自己处理异常不会像 coroutineScope() 那样抛出。
  • 使用场景:主要用于启动多个独立任务。
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 runInterruptible()

  • 异常:会连锁取消子协程、兄弟协程和父协程。
  • 上下文:指定上下文
  • 使用场景:底层使用 withContext() 实现,但会检查 Job 状态,一旦协程取消会调用 Thread.interrupt() 来释放线程,用来包装那些不支持挂起的 Java IO操作,避免作用域中没有挂起点不会响应取消。
public suspend fun <T> runInterruptible(
    context: CoroutineContext = EmptyCoroutineContext,
    block: () -> T
): T
suspend fun readFile() = runInterruptible(Dispatchers.IO) {
    Thread.sleep(5000)
    File("data.txt").readText()
}

5.5 withTimeout() 

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

public suspend fun <T> withTimeout(

        timeout: Duration,

        block: suspend CoroutineScope.() -> T

): T

withTimeout(1000) {
    println("")
}

5.6 withTimeoutOrNull() 

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

public suspend fun <T> withTimeoutOrNull(

        timeMillis: Long,

        block: suspend CoroutineScope.() -> T

): T? 

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

六、创建协程的区别

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

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

MainScope( )

ViewModel.viewModelScope

LifecycleOwner.lifecycleScope

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

withTimeout( )

withTimeoutOrNull( )

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

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

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

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

携带来自CoroutineScope的协程上下文

异常通过Job传递给父协程

构建器是并发执行的,但可调用 join()、await()

携带Continuation的协程上下文

异常像普通函数那样抛出

挂起函数是阻塞的,按顺序执行

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。与内部直接子协程主从,与外部协同。向下单向传播。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值