一、启动函数
1.launch
Coroutine如何创建呢?有两种方式,分别为launch与async
launch: 开启一个新的Coroutine,但不返回结果
async: 开启一个新的Coroutine,但返回结果
还是上面的例子,如果我们需要执行fetch方法,可以使用launch创建一个Coroutine
1 private fun excute() {
2 CoroutineScope(Dispatchers.Main).launch {
3 fetch()
4 }
5 }
2.async
基本用法
在 Kotlin 协程库中,async {} 是一个创建并启动新协程的函数。它接受一个 lambda 表达式作为参数,这个 lambda 表达式定义了协程的执行逻辑。async {} 会立即返回一个 Deferred 对象,这个对象代表了异步计算的结果。
val deferred = async {
// 异步计算的逻辑
computeSomething()
}
val result = deferred.await() // 获取异步计算的结果
await方法
async返回结果,如果要等所有async执行完毕,可以使用await或者awaitAll
1 private suspend fun fetchAll() {
2 coroutineScope {
3 val deferredFirst = async { get("first") }
4 val deferredSecond = async { get("second") }
5 deferredFirst.await()
6 deferredSecond.await()
7
8// val deferred = listOf(
9// async { get("first") },
10// async { get("second") }
11// )
12// deferred.awaitAll()
13 }
14 }
所以通过await或者awaitAll可以保证所有async完成之后再进行resume调用。
注意,await()
是一个挂起函数,它会挂起当前的协程,直到异步计算完成。
Deferred 对象
async {} 返回的 Deferred 对象是一个重要的概念。Deferred 是一个表示异步计算结果的接口,它继承自 Job 接口。Job 接口代表了一个可取消的计算,它有一个 cancel() 函数用于取消计算,以及一个 isCancelled 属性用于检查计算是否已被取消。
Deferred 接口添加了一个 await() 函数,这个函数会挂起当前的协程,直到异步计算完成,并返回计算的结果。如果异步计算抛出了一个异常,那么 await() 函数会重新抛出这个异常。
val deferred = async {
// 异步计算的逻辑
computeSomething()
}
try {
val result = deferred.await() // 获取异步计算的结果
} catch (e: Exception) {
// 处理异步计算的异常
}
async {}
也支持错误处理。如果在 async {}
的 lambda 表达式中抛出了一个异常,那么这个异常会被封装在 Deferred
对象中。当我们调用 await()
函数时,如果 Deferred
对象中有异常,那么 await()
函数会重新抛出这个异常。
val deferred = async {
// 在这里抛出一个异常
throw RuntimeException("Something went wrong")
}
try {
val result = deferred.await() // 这里会重新抛出异常
} catch (e: Exception) {
// 在这里处理异常
println(e.message)
}
传入调度器
async {}
函数接受一个可选的上下文参数,这个参数可以用于指定新协程的上下文和调度器。如果没有指定上下文,那么新协程将继承父协程的上下文。
val deferred = async(Dispatchers.IO) {
// 在 IO 调度器上执行异步计算
computeSomething()
}
val result = deferred.await() // 获取异步计算的结果
在这个例子中,我们在调用 async {}
时,传入了 Dispatchers.IO
作为上下文。这意味着新协程将在 IO 调度器上执行,这个调度器是专门用于 IO 密集型任务的。
注意事项
- 在使用 async {} 时,有一些最佳实践可以帮助我们避免常见的问题。首先,我们应该尽量减少使用 async {} 的数量。因为 async {} 会创建新的协程,如果我们在一个循环中使用 async {},那么我们可能会创建大量的协程,这可能会导致系统资源的浪费。相反,我们应该尽量使用 map、filter 等函数来处理集合,而不是为集合中的每个元素都创建一个新的协程。
-
我们应该避免在 async {} 中执行长时间运行的操作。因为 async {} 会立即返回,如果我们在 async {} 中执行长时间运行的操作,那么我们可能会在获取结果之前就退出了函数,这可能会导致 Deferred 对象被提前回收,从而导致异步计算的结果被丢失。
-
我们应该避免在 async {} 中修改共享的状态。因为 async {} 会并行执行,如果我们在 async {} 中修改共享的状态,那么我们可能会遇到并发问题。为了避免这种问题,我们应该尽量使用不可变的数据结构,或者使用锁来保护共享的状态。
val data = listOf(1, 2, 3, 4, 5)
val results = data.map { element ->
async {
// 对每个元素进行异步计算
computeSomething(element)
}
}.awaitAll() // 获取所有异步计算的结果
在这个例子中,我们首先创建了一个数据列表。然后,我们使用 map 函数和 async {} 来对列表中的每个元素进行异步计算。最后,我们使用 awaitAll() 函数来获取所有异步计算的结果。注意,我们没有在 async {} 中修改任何共享的状态,也没有执行任何长时间运行的操作。
局限性
- 尽管 async {} 是一个强大的工具,但它也有一些局限性。首先,async {} 会立即返回,这意味着我们不能在 async {} 中执行需要阻塞的操作,如读取文件或网络请求。为了处理这种情况,我们可以使用 suspend 函数,或者使用其他的并发工具,如 Future 或 Promise。
- async {} 不能在没有协程的上下文中使用。也就是说,你不能在一个普通的函数中调用 async {},除非这个函数已经在一个协程中了。为了解决这个问题,我们可以使用 runBlocking {} 函数来创建一个协程的上下文。
- async {} 的错误处理模型可能会让人困惑。如果在 async {} 的 lambda 表达式中抛出了一个异常,那么这个异常会被封装在 Deferred 对象中,而不是立即被抛出。这意味着,如果我们忘记了调用 await() 函数,那么我们可能会错过这个异常。为了避免这种情况,我们应该总是在 await() 函数的调用处处理可能会发生的异常。
val deferred = async {
// 在这里抛出一个异常
throw RuntimeException("Something went wrong")
}
// 在这里,我们忘记了调用 await() 函数,所以我们错过了这个异常
在这个例子中,我们在 async {}
的 lambda 表达式中抛出了一个异常。然后,我们忘记了调用 await()
函数,所以我们错过了这个异常。
性能
在使用 async {} 时,我们也需要考虑性能。因为 async {} 会创建新的协程,如果我们创建了大量的协程,那么这可能会导致系统资源的浪费。为了避免这种情况,我们应该尽量减少使用 async {} 的数量,或者使用 coroutineScope {} 函数来限制协程的数量。
coroutineScope {
val deferreds = List(1000) {
async {
// 在这里执行异步计算
computeSomething(it)
}
}
val results = deferreds.awaitAll() // 获取所有异步计算的结果
}
在这个例子中,我们使用 coroutineScope {}
函数来创建一个协程范围。在这个范围内,我们创建了 1000 个协程。由于所有的协程都在同一个范围内,所以它们会共享相同的上下文和调度器,这可以减少系统资源的消耗。
3.launch和async的区别
在 Kotlin 协程库中,async {} 和 launch {} 是两个最常用的创建协程的函数。这两个函数的主要区别在于它们的返回值:async {} 返回一个 Deferred 对象,代表了一个可以被稍后获取结果的异步计算,而 launch {} 返回一个 Job 对象,代表了一个可以被取消的计算。
val job = launch {
// 这里的代码没有返回值
doSomething()
}
job.cancel() // 可以取消这个计算
val deferred = async {
// 这里的代码有返回值
computeSomething()
}
val result = deferred.await() // 可以获取这个计算的结果
在这个例子中,我们首先使用 launch {}
来启动一个新的协程,然后我们可以调用 cancel()
函数来取消这个协程。然后,我们使用 async {}
来启动一个新的协程,我们可以调用 await()
函数来获取这个协程的结果。
4.async和 launch的选择
在 Kotlin 协程中,async {} 和 launch {} 是两个最常用的创建协程的函数。那么,我们应该在什么情况下使用 async {},在什么情况下使用 launch {} 呢?
一般来说,如果我们需要获取协程的结果,那么我们应该使用 async {}。因为 async {} 返回一个 Deferred 对象,我们可以使用这个对象的 await() 函数来获取协程的结果。
如果我们不需要获取协程的结果,或者我们只是想启动一个并发的操作,那么我们应该使用 launch {}。因为 launch {} 返回一个 Job 对象,我们可以使用这个对象的 cancel() 函数来取消协程。
val deferred = async {
// 我们需要获取这个计算的结果,所以我们使用 async {}
computeSomething()
}
val result = deferred.await() // 获取异步计算的结果
val job = launch {
// 我们不需要获取这个操作的结果,所以我们使用 launch {}
doSomething()
}
job.cancel() // 取消这个操作
在这个例子中,我们首先使用 async {}
来启动一个需要获取结果的异步计算。然后,我们使用 launch {}
来启动一个不需要获取结果的并发操作。
二、挂起和恢复函数
1.suspend
suspend
是协程的关键字,每一个被suspend
修饰的方法都必须在另一个suspend
函数或者Coroutine
协程程序中进行调用。
suspend
是一个关键的关键字,它用于声明一个可以被挂起的函数。挂起函数可以在执行过程中被暂停,并在稍后的某个时间点恢复执行。这使得我们可以在挂起函数中执行长时间运行的操作,而不会阻塞当前的线程。
为什么suspend
修饰的方法需要有这个限制呢?不加为什么就不可以,它的作用到底是什么?
这里涉及到一种机制俗称CPS(Continuation-Passing-Style)
。每一个suspend
修饰的方法或者lambda
表达式都会在代码调用的时候为其额外添加Continuation
类型的参数。
@GET("/v2/news")
suspend fun newsGet(@QueryMap params: Map<String, String>): NewsResponse
上面这段代码经过CPS
转换之后真正的面目是这样的
@GET("/v2/news")
fun newsGet(@QueryMap params: Map<String, String>, c: Continuation<NewsResponse>): Any?
经过转换之后,原有的返回类型NewsResponse
被添加到新增的Continutation
参数中,同时返回了Any?
类型。这里可能会有所疑问?返回类型都变了,结果不就出错了吗?
其实不是,Any?
在Kotlin
中比较特殊,它可以代表任意类型。
当suspend
函数被协程挂起时,它会返回一个特殊的标识COROUTINE_SUSPENDED
,而它本质就是一个Any
;当协程不挂起进行执行时,它将返回执行的结果或者引发的异常。这样为了让这两种情况的返回都支持,所以使用了Kotlin
独有的Any?
类型。
返回值搞明白了,现在来说说这个Continutation
参数。
首先来看下Continutation
的源码
public interface Continuation<in T> {
/**
* The context of the coroutine that corresponds to this continuation.
*/
public val context: CoroutineContext
/**
* Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
* return value of the last suspension point.
*/
public fun resumeWith(result: Result<T>)
}
context
是协程的上下文,它更多时候是CombinedContext
类型,类似于协程的集合,这个后续会详情说明。
resumeWith
是用来唤醒挂起的协程。前面已经说过协程在执行的过程中,为了防止阻塞使用了挂起的特性,一旦协程内部的逻辑执行完毕之后,就是通过该方法来唤起协程。让它在之前挂起的位置继续执行下去。
所以每一个被suspend
修饰的函数都会获取上层的Continutation
,并将其作为参数传递给自己。既然是从上层传递过来的,那么Continutation
是由谁创建的呢?
其实也不难猜到,Continutation
就是与协程创建的时候一起被创建的。
GlobalScope.launch {
}
launch
的时候就已经创建了Continutation
对象,并且启动了协程。所以在它里面进行挂起的协程传递的参数都是这个对象。
简单的理解就是协程使用resumeWith
替换传统的callback
,每一个协程程序的创建都会伴随Continutation
的存在,同时协程创建的时候都会自动回调一次Continutation
的resumeWith
方法,以便让协程开始执行。
协程的suspend关键字在Kotlin协程中扮演着至关重要的角色。以下是关于其作用和工作原理的详细解释:
作用
- 挂起协程执行:使用suspend关键字定义的函数被称为挂起函数。挂起函数能够在不阻塞线程的情况下,暂时停止协程的执行。当协程被挂起时,它会释放当前线程的执行权,允许其他任务或协程在该线程上继续执行。
- 切换线程:协程可以在挂起时切换到其他线程(如IO线程)继续执行耗时操作,而不需要主线程等待。这对于避免线程阻塞和提高应用程序的响应性至关重要。
- 恢复协程执行:当挂起函数中的耗时操作完成后,协程可以从挂起点恢复执行,继续处理后续的逻辑。
工作原理
- 挂起:当协程执行到挂起函数时,它会生成一个Continuation对象。这个对象包含了协程当前的状态和需要恢复执行时的上下文信息。然后,协程会将执行权释放给调度器,等待耗时操作完成。
- 执行耗时操作:挂起函数通常会切换到另一个线程(如IO线程)来执行耗时操作,如网络请求或文件读写。这些操作不会阻塞主线程,因此主线程可以继续执行其他任务。
- 恢复执行:当耗时操作完成后,挂起函数会通过调用Continuation的resume方法或resumeWith方法来恢复协程的执行。此时,协程会从挂起点继续执行后续的逻辑。
注意事项
- 只能在协程内调用:挂起函数只能在协程体内或其他挂起函数内调用。在非协程环境中调用挂起函数会导致编译错误。
- 非阻塞式挂起:虽然协程在挂起时看起来像是阻塞的,但实际上它是非阻塞的。因为挂起函数会释放当前线程的执行权,并允许其他任务或协程在该线程上继续执行。
- 支持取消和异常处理:Kotlin协程提供了对取消操作和异常处理的支持。当协程被取消时,它会立即停止执行并释放资源。同时,协程中的异常也会通过特定的机制进行捕获和处理。
综上所述,suspend关键字在Kotlin协程中起到了挂起和恢复协程执行的作用,并通过生成Continuation对象和切换线程来实现这一功能。它是Kotlin协程实现非阻塞异步编程的关键机制之一。
2.resume
三、协程异常处理函数
CoroutineExceptionHandler
推荐文章