如何安全地使用Kotlin协程

本文介绍了如何在Android开发中安全使用Kotlin协程,包括利用CoroutineScope跟踪和终止协程,防止协程泄漏。通过示例展示了在ViewModel中启动协程、使用coroutineScope跟踪多个协程以及处理执行失败和错误信号的方法,强调了结构化并发在避免协程泄漏中的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

《五分钟说清楚Kotlin协程和Android的那点事》一文中,我们对Kotlin协程可以解决什么问题进行了深入的分析。在本文中,我们将探讨如何安全地使用Kotlin协程。

跟踪协程

协程可以很好地解决两个常见的编程问题:

  • 处理长时间运行的任务;
  • 确保主线程不被阻塞。

协程通过添加suspend和resume操作来实现上述功能。当特定线程上的所有协程都被挂起时,该线程就可以执行其他的任务了。

但是,协程本身并不能跟踪它正在执行的任务。协程本身的开销很小。因此,创建大量的协同并且将它们全部挂起是没有问题的。但是,通常情况下,协程执行的任务却通常有很大的开销,例如读取文件或发送网络请求等。

使用代码手动跟踪数量庞大的协程是非常困难的。尽管开发者可以尝试手动跟踪协程,并确保它们执行完成或被终止。但是这样的代码很繁琐且容易出错。如果代码存在bug,那么我们将失去对协程的跟踪,这就是我们所说的协程泄漏。

协程泄漏类似于内存泄漏,但情况更糟。被泄露的协程,除了占用内存外,它还有可能被意外唤醒,并继续使用CPU、磁盘甚至发送网络请求。

泄漏的协程会浪费内存、CPU和磁盘,甚至发送不必要的网络请求。

为了避免协程泄漏,Kotlin引入了结构化并发。结构化并发是避免协程泄露的最佳实践。如果我们遵循这个最佳实践,就可以有效地跟踪协程,并避免协程泄露。

在Android开发中,我们可以使用结构化并发来做三件事:

  • 在运行时跟踪任务;
  • 必要时终止任务;
  • 当协程执行失败时发出错误信号。

接下来,我们将深入探讨这些,看看结构化并发是如何帮助我们跟踪协程并避免协程泄漏的。

使用作用域终止协程

在Kotlin中,协程必须在CoroutineScope中运行。 CoroutineScope可以跟踪协程,包括已被挂起的协程。与调度程序(dispatcher)不同,CoroutineScope实际上并不执行协程,而只是确保协程不会被泄露。

为了确保所有的协程都被跟踪,Kotlin不允许在没有CoroutineScope的情况下启动新的协程。我们可以将CoroutineScope视为ExecutorService的轻量级版本。它使得我们能够启动新的协程,并轻松地挂起或恢复这些协程。

CoroutineScope会确保通过它启动的协程都被跟踪,并且可以在适当的时候终止这些协程。这对Android开发来说非常重要。因为开发者需要确保在用户离开某个界面时清理掉它所启动的所有协程。

CoroutineScope会跟踪协程,并且可以在适当的时候终止这些协程。

启动新的协程

开发者不能在任意位置调用suspend函数。启动协程有两种方法:

  • 启动构建器(launch builder)将启动一个“即用即弃”的新协程,这意味着它将不会将结果返回给调用方;
  • 异步生成器(async builder)将启动一个新的协程,并可以通过被称为await的suspend函数返回结果。

在通常情况下,从常规函数启动协程的正确做法是使用launch操作。我们应该使用协程作用域并配合launch操作来启动协程。

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

我们可以将launch操作视为常规函数和协程之间的桥梁。在launch操作内部,我们可以调用suspend函数并确保主线程安全。

Launch是从常规函数到协程的桥梁。

Launch和async之间的最大区别是它们如何处理异常的。async认为我们最终会调用await来获得结果(或异常),因此默认情况下不会触发异常。这意味着,如果我们使用async来启动新的协程,它将忽略异常。

由于Launch和async仅在CoroutineScope上可用,因此我们创建的任何协程都会被跟踪。Kotlin不允许创建未被跟踪的协程,这样可以避免协程泄漏。

从ViewModel启动协程

在Android应用中,将CoroutineScope与特定用户界面关联起来是很常见的。这样可以避免协程泄漏,也可以确保被终止的Activity或Fragment不会执行不必要的任务。当用户离开某个界面后,与该界面关联的CoroutineScope可以自动终止它创建的所有协程。

结构化并发保证当作用域终止时,其跟踪的所有协程都被终止。

当我们将协程与Android Architecture Components集成时,通常需要在ViewModel中启动协程。这是很自然的,因为这里是大多数任务被创建并执行的地方。

要在ViewModel中使用协程,我们可以使用viewModelScope,它是lifecycle-viewmodel-ktx:2.1.0-alpha04.viewModelScope提供的扩展属性

让我们看看这个代码示例:

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

viewModelScope将自动终止由此ViewModel启动的任何协程。通常来说这是正确的,如果我们尚未获取某个文档,当用户关闭应用程序时,我们需要终止协程。

因此,当需要协程拥有和ViewModel一样长的生命周期时,我们可以使用viewModelScope来从常规函数切换到协程。 由于viewModelScope可以自动终止协程,因此编写无限循环并不会产生协程泄漏。

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
        }
    }
}

通过使用viewModelScope,我们可以确保任何任务(甚至是无限循环)在不再被需要的时候被终止。

跟踪协程

使用协程是非常容易的。大多数时候,我们只需要启动协程、执行相关任务并处理返回结果。

有时候,情况会变得稍微复杂一些。假设我们需要在协程中同时发出两个网络请求,在这种情况下我们需要启动更多的协程。

在suspend函数中启动多个协程可能会造成协程泄漏。因为suspend函数的调用者可能不知道多个协程被创建了。为了解决这个问题,结构化并发可以确保suspend函数返回时,所有的任务都已执行完毕。

结构化并发保证当suspend函数返回时,其所有任务都已执行完毕。

这是使用coroutineScope获取两个文档的代码示例:

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

在上述代码示例中,同时从网络中获取了两个文档。第一个协程是通过launch启动的,该协程不会将结果返回给调用方。第二个文档是使用async方式启动的,因此该文档可以被返回给调用方。

coroutineScope和supervisorScope可让我们从suspend函数中安全地启动协程。

值得注意的是,上述代码并不会显式地等待任何协程。在协程运行的过程中,fetchTwoDocs函数就像是返回了一样。

为了避免协程泄漏,我们希望确保suspend函数返回时,其所有的任务都已执行完毕。这意味着fetchTwoDocs启动的两个协程必须在fetchTwoDocs返回之前完成。

Kotlin确保协程不会从fetchTwoDocs的coroutineScope构建器中泄漏。coroutineScope生成器将suspend自身,直到在其内部启动的所有协程都执行完毕为止。因此,在coroutineScope构建器中启动的所有协程执行完毕之前,我们不会从fetchTwoDocs返回。

跟踪很多很多的协程

我们已经探讨了跟踪一个和两个协程的情况,接下来我们将尝试跟踪1000个协程。让我们看一下这个动画:
在这里插入图片描述
在上述代码中,我们在coroutineScope构建器内部启动了1000个协程。由于我们处于suspend函数中,因此loadLots函数必然已经位于某个CoroutineScope中了。这个CoroutineScope可能是viewModelScope作用域,也可能是其他作用域。无论它是什么作用域,coroutineScope构建器都将使用它作为父作用域。

在coroutineScope块内,launch将在新作用域内启动协程。协程启动完成之后,新作用域将对其进行跟踪。最终,在coroutineScope内部启动的所有协程执行完成之后,loadLots就可以返回了。

coroutineScope和supervisorScope将等待其内部所有的协程执行完毕。

通过使用coroutineScope或supervisorScope,我们可以从任何suspend函数中安全地启动协程。即使启动新的协程,我们也不需要担心协程泄漏,因为函数调用者将始终被挂起,直到所有的协程执行完毕。

更加重要的是,coroutineScope将创建一个子作用域。如果父作用域被终止,所有的协程都将被终止。如果调用者使用的是viewModelScope,那么当用户离开某个用户界面时,所有的1000个协程将被自动终止。

supervisorScope和coroutineScope的主要区别在于,只要任何子协程失败,coroutineScope就会终止。因此,如果一个网络请求失败,则所有其他请求将立即被取消。相反,如果我们希望即使有请求失败也继续执行其他请求,则可以使用supervisorScope。当其中一个网络请求失败时,supervisorScope不会终止其他协程。

执行失败与错误信号

在协程中,执行失败是通过抛出异常来发出错误信号的。suspend函数的异常将通过resume重新发送给调用者。但是,在某些情况下,协程可能会丢失错误信号。

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

上述代码声明了一个不相关的作用域,它将在没有结构化并发的情况下启动一个新的协程。请记住,在suspend函数中引入不相关的协程作用域并不遵循结构化并发的最佳实践。

该错误信号将在上述代码中被丢失,因为async假定我们最终将调用await以其重新抛出异常。但是,我们实际上从未调用过await,因此,该异常将永远不会被重新抛出。

结构化并发保证当协程发生错误时,将正确地通知其调用方或作用域。

如果我们对上述代码使用结构化并发,则错误信号将被正确地发送给调用者。

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

由于coroutineScope将等待所有子协程执行完毕,因此当协程执行失败时coroutineScope可以得到通知。如果由coroutineScope启动的协程抛出异常,则coroutineScope可以将其重新抛给给调用方。由于我们使用的是coroutineScope而不是supervisorScope,因此抛出异常后,coroutineScope会立即终止所有其他子协程。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值