Andorid协程三部曲之二:开始使用协程

本文是Android协程系列的第二篇,主要介绍如何开始使用协程,包括跟踪协程、使用CoroutineScope取消协程、批量任务处理以及处理协程中的错误。通过结构化并发,可以有效避免协程泄漏,确保协程安全地运行并与ViewModel生命周期绑定,防止工作泄露。
摘要由CSDN通过智能技术生成

Coroutines on Android (part II): getting started
这是Andoid 协程系列文章中的第二篇。这篇将关注协程是怎样开始运行的。

1. 跟踪协程

在第一篇文章中,讲述了协程擅长于解决的是哪类问题。回顾一下,协程擅长于解决的是如下两类常见的问题:

  1. 会阻塞主线程的长时间耗时任务
  2. 主线程安全。任何suspend 函数都可以在主线程被安全的调用。

为了解决上述两个问题,协程通过在普通函数上增加suspend 和 resume。当特定线程上的所有协程都被挂起的时候,该线程可以自由的做其他工作

然而,协程它们自己并不能跟踪自己正在做的事。大量的协程(甚至说成千上万)在同一时间都suspend也是没有问题的。尽管协程是轻量级、它们所要做的事却是重量级的,比如读取文件,网络请求等。

手动使用代码来追踪1000个协程是相当困难的。你可以尝试追踪,并且手动来确保它们已经完成或是被取消,但是这样的代码可能很冗长或是容易出错。如果代码不够完善,有可能对协程失去追踪,这可以称之为 work leak

work leak和内存 泄漏很像,但也更糟糕。如果一个协程缺失追踪和管理,除了会涉及内存,也会包括CPU/磁盘 等资源的占用,甚至还发起网络请求。

协程泄漏会浪费内存,cpu,磁盘,甚至会继续发起并不需要的网络请求

为了解决协程泄漏,kotlin引入了结构化的并发,结构化并发是语言特性和最佳实践的组合,如果遵循这些特性,可以帮助跟踪协程运行中的所有工作。

在Android 上,我们可以使用结构化并发来做以下三件事

  1. 当不再需要的时候,协程是可以取消的
  2. 在运行的时候,可以追踪到是怎样工作的
  3. 协程失败的时候,可以发出错误信号

让我们再深入探讨以上三个方面,看看结构化并发是怎样保证我们不会缺失对协程的追踪从而避免work leak。

2. 使用scopes 取消协程

在Kotlin中,协程必须运行在CoroutineScope里。CoroutineScope会负责跟踪协程,甚至是suspend的协程。不像我们在其他文章中提到的Dispatchers,CoroutineScope并不执行协程,它仅仅是保证你不会跟掉协程。

为了确保所有的协程能被追踪到,Kotlin 是不会允许在没有CoroutineScope的情况里开启一个新协程的。你可以把CoroutineScope 想象成一个轻量级但是又非常强大的ExecutorService。CoroutineScope赋予了启动协程的能力。

CoroutineScope 既能跟踪所有的协程,也可以取消在它里面启动的协程。

1. 开启协程的方法

要记住,并不是在任何地方都可以调用suspend 函数。suspend 和 resume 机制要求你从普通函数上切换到协程中。

有两种方法启动协程,它们有不同的使用用途

  1. 使用launch 将会构建一个不带返回结果给调用者的协程
  2. 使用async 将会构建一个当调用await会有返回结果给调用者的协程

在大多数情况下,在普通函数上启动协程话的可以使用launch。因为普通函数上没法调用await ,因此也没必要使用async。稍后会讨论在什么情况下有必要使用async

lancun又是在scope中使用的,因此,如下方式启动协程

lifecycleScope.launch{
 // This block starts a new coroutine 
    // "in" the scope.
    // 
    // It can call suspend functions
	request()
        }
 GlobalScope.launch {
        request()
        }

你可以把launch想象成一座桥,把普通函数切换到协程的环境中。在launch 里,你可以代用suspend 函数,并且是主线程安全的。

注意:launch和async 之间的一个巨大差异就是如何处理异常。async希望你最终调用await 来获取结果或是异常,这样它就不用默认的抛出异常了。这也意味着,如果你使用async来开启一个协程,有异常也会悄无声息的丢弃异常。

既然launch 和 async只能在CoroutineScope上使用,所以你创建的任何协程都将会被这个scope追踪到。Kotlin 不会让你创建一个追踪不到的协程,这会导致work leaks。

2.在ViewModel中开启协程

如果一个CoroutineScope 负责记录所有协程,launch负责 创建新协程,那么你应该在哪调用launch并将它放在scopes中?什么时候取消scope中开始的所有协程?

在Android上,将CoroutineScope 和用户屏幕关联起来很合理。这会让你避免用户已经离开某个Activity或是Fragment,造成的协程泄漏或是做一些额外的工作。当用户离开某个页面,与之关联的CoroutineScope 应该取消它里面的工作。

结构化并发保证了当一个scope 去掉的时候,里面所有的协程也会取消。

当协程和Andorid架构组件结合的时候,你会想到在ViewModel中使用launch 。这个一个很合情合理的地方,因为viewmodle 负责是负责数据业务处理的地方,并且和activity/fagment声明周期绑定,而且屏幕旋转的时候,协程并不会被kill。

在viewModle 中使用协程,可以从lifecycle-viewmodel-ktx:2.1.0-alpha04.viewModelScope 中使用viewModelScope的扩展属性。

class MyViewModel(): ViewModel() {
    fun userNeedsDocs() {
        // Start a new coroutine in a ViewModel
        viewModelScope.launch {
            fetchDocs()
        }
    }
}

当ViewModel 销毁的时候,viewModelScope 会自动取消它里面的协程。而且也会递归的取消所有协程,也即在一个scope中,你开启了一个协程,然后在这个协程中又开启了另外一个协程,它们都会被取消。
注意:当协程被挂起的时候,通过抛出CancellationException来协作地取消协程。

所以,当你需要一个协程运行生命周期和viewmodel保持一致,使用viewModelScope 将普通函数切换到协程世界中。那么viewModle clear之后,viewModelScope 将会自动帮你取消所有的协程来避免work leaks。甚至你在协程中写个死循环都没关系,它也会帮你取消的。

fun runForever() {
    // start a new coroutine in the ViewModel
    viewModelScope.launch {
        // cancelled when the ViewModel is cleared
        while(true) {
            delay(1_000)
            // do something every second
        }
    }
}

3. 跟踪协程工作

在发起网络请求/操作数据库时开启一个协程是很好的。但是有时候,你的业务可能会更复杂一点。比如在一个协程中你可能同时需要做两次网络请求,或是需要启动多个协程。

为了创建更多的协程,任何suspend函数都可以调用coroutineScope 或是supervisorScope。

在任意地方启动协程是有可能造成潜在的work leaks.调用者如果不知道新协程在哪,当然也就不知道如何追踪这些协程了。

结构化并发帮助我们修复这个问题。也就是说,它提供了一种保障,当一个suspend函数返回的时候,它的任务也就完成了。

以下是用协程获取两次文件的example:

suspend fun fetchTwoDocs() {
    coroutineScope {
        launch { fetchDoc(1) }
        async { fetchDoc(2) }
    }
}

这个例子中,是协程里面再开启了协程,代码也没有显示的指明它会等待某个新协程,它看上去就像协程运行的时候,fetchTwoDocs会返回结果一样(可能是表达像在写同步代码一样?)

为了构建结构化并发并避免work leaks,我们要确保当像fetchDocs这样suspend 函数返回的时候,协程所有的工作都做完了。这也意味着,在fetchTwoDocs返回的之前,这两个协程已经完成了。

kotlin会确保用coroutineScope 构建的fetchDocs不会leak ,coroutineScope 构建器会 挂起自己直到它里面启动的所有协程都工作完毕。也因为此,没有路径可以从fetchTwoDocs返回,除非coroutineScope 里面所有的协程都工作完毕。

1. 大批量的工作

我们分析了一个和两个的协程,现在我们来看成千上万的协程.

suspend fun loadLots(){
        coroutineScope{
            repeat(1000){
                launch{
                    fetchTwoDocs()
                }
            }
        }
    }

这个代码展示了同时发起1000次的网络请求,在实际使用场景中,并不推荐这样做,因为你会占用很多资源。

在coroutineScope 内部,我们使用launch 创建了1000个协程,现在捋一捋这行代码。因为我们有suspend 函数,那么它就必须用CoroutineScope开启协程来调用。我们可能不了解这个coroutineScope是啥, 可能是viewModelScope或是其他类型的CoroutineScope,不管是啥类型的coroutineScope,它都是它里面创建的coroutineScope的父对象。

在coroutineScope 块内,使用launch 在一个新的coroutineScope 中启动新的协程,当这个launch里的协程执行完毕,它的这个新的coroutineScope会追踪记录到此。到最后,coroutineScope 里面所有的协程执行完毕,loadLots就可以返回。

注意:scopes 和 coroutines 之间的父-子关系是由Job这个对象创建的。
coroutineScope和 supervisorScope 将会等待其child coroutines 完成

这里还有许多内幕,但是重要的是,使用coroutineScope 或是 supervisorScope ,你可以安全的使用launch 启动协程调用suspend 函数。尽管这也会启动一个新的协程,你也不会 leak work,因为你会挂起调用者,直到写的协程执行完毕。

真正cool的是coroutineScope将会创建一个子scope。因此当这个父scope 取消,会传递到其子scope也都取消。如果是viewModelScope调用,这一千个协程都会被随之用户跳转离开这个页面而自动取消。

在我们看协程的异常之前,可以花时间思考下supervisorScope vs coroutineScope.它们主要不同在于,当coroutineScope里面的任意一个子coroutines 失败的时候,coroutineScope将会取消。也就是说,当一个网络请求失败,所有的其他网络请求都会被立刻取消。但是如果你想其它网络请求还能继续的话,你就可以使用supervisorScope。

4. 协程失败发出错误信号

在协程中,错误信号是通过抛出异常发出的。就和普通函数一样。一个挂起的函数发生异常,就会在恢复的时候抛给调用者。这和普通函数一样,你可以不受限于try/catch来处理异常。如果愿意,还可以使用你喜欢的其他方式来处理。

然而,在协程中也有可能丢失异常的情况

val unrelatedScope = MainScope()
// example of a lost error
suspend fun lostError() {
    // async without structured concurrency
    unrelatedScope.async {
        throw InAsyncNoOneCanHearYou("except")
    }
}

此代码声明了一个不相关的协程作用域,它将启动一个没有结构化并发性的新协程。这个异常丢失,是因为async 假定当程序抛出异常的时候,你最终都会调用await 。然而,如果你不调用await,这个异常将被永久存储,耐心地等待被引发。

 suspend fun lostError(){
        MainScope().async {
            throw Exception("except")
        }
    }

    suspend fun foundError() {
        coroutineScope {
            async {
                throw Exception("throw")
            }
        }
    }

    suspend fun foundError1(){
        MainScope().async {
            throw Exception("except")
        }.await()
    }

    suspend fun foundError2(){
        MainScope().launch {
            throw Exception("except")
        }
    }

如果用了结构化并发,抛出的异常就会被返回给调用者,如foundErrors.父coroutineScope 将会等待所有的子协程执行完毕,当子协程失败的时候,它也会收到通知。如果一个在coroutineScope 里面的协程抛出了异常,这个coroutineScope 可以把异常传递给调用者。因为我们使用的是coroutineScope 而不是supervisorScope,异常发生可以立马取消其他所有的协程。

5. 使用结构化并发

以下将会介绍结构化并发并展示它是如何让我们的代码适用在viewModel上并避免work leaks。

如果我们不适用结构化并发,那有可能会意外造成调用者并不知道的一些work leaks,比如该取消的请求没有取消掉,该抛出的异常没有抛给调用者等等,这会让我们的代码有时候产生一些莫名其妙的bug。

你可以通过不相关联的CoroutineScope(首字母是大写C) ,或是 GlobalScope 来创建非结构的并发,但是这种情况是适用于你知道这个scope里的协程存活得比调用的scope还要久。一个很好的办法就是你要约束自己的代码来保证你能追踪到这些非结构化的协程,比如自己处理异常,自己处理取消等等。

如果你有非结构化并发的经验,则需要一些时间来习惯结构化并发,结构化并发让你在和suspend 函数打交道的时候更加安全和容易。要尽可能多的使用结构化并发,它会让代码更加容易理解和少出奇怪的错误。

最后,我再列举机构化并发给我们解决的如下三个问题:

  1. 取消该取消的工作
  2. 当一个suspend 函数返回,说明它所有的工作都做完了
  3. 当协程异常,起scope和调用者都会受到通知

一句话,结构化并发让我们的代码更简单和安全,避免leaking work。

6. 下一篇:

Coroutines On Android (part III): real work

这一篇我们讲述了在 Android viewModle 中如何开启一个协程,并怎样使用结构化并发来让我们的代码少出错。

下一篇,我们将探讨如何在实际场景中使用协程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值