Kotlin 协程是一种轻量级的并发机制,它允许开发者以更简洁、直观的方式编写异步代码。Kotlin 协程是基于 Kotlin 的标准库和 Kotlin 编译器的支持。
0x00 基本原理
以下概念组成了 Kotlin 协程的基本原理:
-
挂起函数(Suspend Functions)
- 挂起函数是协程的基础。这些函数可以暂停协程的执行,而不阻塞线程。挂起函数通常会在其内部进行一些可能会阻塞的操作(如网络请求、文件读写等),然后恢复协程的执行。
- 挂起函数通过
suspend
关键字标记,它们只能在协程或其他挂起函数中调用。
-
协程构建器(Coroutine Builders)
- Kotlin 提供了多个协程构建器,如
launch
、async
和runBlocking
,用于创建和启动协程。 launch
用于启动一个新的协程,它不返回结果。async
用于启动一个新的协程,并返回一个Deferred
对象,可以通过调用其await()
方法来获取结果。runBlocking
用于在当前线程上启动一个新的协程,并阻塞当前线程直到协程完成。
- Kotlin 提供了多个协程构建器,如
-
协程上下文(Coroutine Context)
- 协程上下文是一个包含协程执行环境的集合,例如线程、协程调度器(CoroutineDispatcher)、协程名称等。
- 协程调度器负责确定协程应该在哪个线程或线程池上执行。
-
协程调度器(Coroutine Dispatchers)
- 协程调度器负责协程的线程调度。Kotlin 提供了多个预定义的调度器,如
Dispatchers.Main
(主线程)、Dispatchers.IO
(用于IO操作)、Dispatchers.Default
(默认的线程池)和Dispatchers.Unconfined
(不限制协程的线程)。 - 开发者可以自定义调度器,以满足特定的并发需求。
- 协程调度器负责协程的线程调度。Kotlin 提供了多个预定义的调度器,如
-
Continuation 对象
- 当一个挂起函数被调用时,它会返回一个
Continuation
对象。这个对象包含了恢复协程执行的代码和状态。 - 挂起函数通过调用
Continuation
对象的resume
方法来恢复协程的执行,并可以传递结果值。
- 当一个挂起函数被调用时,它会返回一个
-
编译器支持
- Kotlin 编译器对协程有特殊处理。它会将挂起函数转换为状态机,通过生成多个回调方法来处理协程的挂起和恢复。
- 这意味着开发者可以以顺序编程的方式编写异步代码,而编译器会负责生成相应的异步执行逻辑。
0x01 异步转同步
假设有一个请求网络的方法,这个方法参数是一个回调接口
kotlin
// 同步请求方法
fun fetchData(callback:OnFetchCallback) {
// 模拟请求过程
Thread.sleep(1000)
val rand = Random.nextInt(0,10)
if (rand > 8) {
// 模拟请求出错的情况
callback.onFail(-1)
} else {
callback.onSuccess("data from network")
}
}
在上古时代,在使用的时候经常会写出这样的代码
kotlin
fetchData(object : OnFetchCallback {
override fun onSuccess(resp: String) {
// 处理数据,也有可能会阻塞线程
processData(resp)
}
override fun onFail(errCode: Int) {
processFail(errCode)
}
})
现代开发不用了,可以考虑使用 suspendCoroutine
与 suspendCancellableCoroutine
协程构建函数,可以将异步代码转换为协程的同步代码
kotlin
suspend fun fetchDataFromNetwork(): String {
return suspendCancellableCoroutine { cont ->
fetchData(object : OnFetchCallback {
override fun onSuccess(resp: String) {
// 使用 resumeWith 将结果返回
cont.resumeWith(Result.success(resp))
cont.cancel()
}
override fun onFail(errCode: Int) {
// 这里处理异常情况,在协程使用的地方可以使用try-catch进行捕获处理
cont.resumeWithException(Throwable("ErrCode: $errCode"))
cont.cancel()
}
})
}
}
suspendCoroutine
与 suspendCancellableCoroutine
的区别就是后者在协程处理完成(例如 resume
之后)可以执行 cancel
方法及时的取消协程,可以有效地避免长时间占用不必要的资源。
在使用的时候,代码逻辑就没有各种callback回调了,例如在 main
是使用的时候,逻辑就非常清晰了。
kotlin
fun main() = runBlocking<Unit> {
try {
println("fetchDataFromNetwork start")
val data = fetchDataFromNetwork()
println("fetchDataFromNetwork result >> $data")
} catch (e: Throwable) {
println("fetchDataFromNetwork error >> $e")
}
}
0x02 并发编程
这个是我最喜欢的一部分了。 假设有一个大列表,这个列表有很多数据都要处理,每一项处理都需要 200 毫秒的处理时间
kotlin
val dataList = listOf(11, 22, 33, 44, 55, 66, 77, 88, 99, 100, 10, 20, 30, 40, 50, 60, 70, 80, 90)
val time = measureTimeMillis {
// 按顺序来处理
dataList.forEach {
processingData(it)
}
}
println("Executed in $time ms")
按顺序来处理,处理10个数据大概需要2秒钟,输出结果为:
shell
processing item >> 11
processing item >> 22
processing item >> 33
processing item >> 44
processing item >> 55
processing item >> 66
processing item >> 77
processing item >> 88
processing item >> 99
processing item >> 100
Executed in 2048 ms
那如何使用协程来优化呢?我们将顺序处理逻辑优化为并发处理
kotlin
suspend fun <T> List<T>.parallelProcessing(parallelism: Int = 10, processBlock: suspend (T) -> Unit) {
withContext(Dispatchers.Default) {
val inputChannel = Channel <T>(parallelism)
// 生产者
launch {
forEach {item->
inputChannel.send(item)
}
inputChannel.close()
}
// 消费者
for (i in 0..parallelism) launch {
for (element in inputChannel) {
processBlock(element)
}
}
}
}
这里使用了扩展函数来实现,参数 parallelism
表示并发处理的数量,这里默认值设置为10,processBlock
是一个挂起函数,在函数体中使用 withContext(Dispatchers.Default)
设置了协程调度器,它负责协程的线程调度,这里设置为 Default
表示使用 CPU
算力。然后定义了缓冲 buffer
大小为10的Channel
信道,即可以通过这个信道同时发送 10 个数据,然后使用了 launch
来启动协程,并使用 Channel
将数据发送到信道中,发送完毕后关闭信道。最后再使用一个 for
循环,启动了另外 10 个协程来处理数据。 简单来说,这个逻辑里面实现了生产者-消费者模型。
看下如何使用的
scss
val time2 = measureTimeMillis {
// 设置并发量10
dataList.parallelProcessing(10) {
// 处理函数
processingData(it)
}
}
println("Exec total time >> $time2")
输出结果为
shell
processing item >> 22
processing item >> 66
processing item >> 55
processing item >> 44
processing item >> 11
processing item >> 33
processing item >> 100
processing item >> 99
processing item >> 88
processing item >> 77
Exec total time >> 221
可以看到处理时间为 200 毫秒左右,即我们的效率提高了10倍!协程帮我们简化了编程模型,提高了效率,也让我们的生活更简单!
当然要在使用并发处理,需要保证列表中数据与数据之间是相对独立的,即不能有相互依赖的关系,例如在处理数据项 1 的时候,需要数据项 5 的处理结果,那么这种情况就不符合了。Anyway,有了这把利器,嘿嘿嘿。