如何使用 Kotlin 协程进行高性能编程

Kotlin 协程是一种轻量级的并发机制,它允许开发者以更简洁、直观的方式编写异步代码。Kotlin 协程是基于 Kotlin 的标准库和 Kotlin 编译器的支持。

0x00 基本原理

以下概念组成了 Kotlin 协程的基本原理:

  1. 挂起函数(Suspend Functions)

    • 挂起函数是协程的基础。这些函数可以暂停协程的执行,而不阻塞线程。挂起函数通常会在其内部进行一些可能会阻塞的操作(如网络请求、文件读写等),然后恢复协程的执行。
    • 挂起函数通过 suspend 关键字标记,它们只能在协程或其他挂起函数中调用。
  2. 协程构建器(Coroutine Builders)

    • Kotlin 提供了多个协程构建器,如 launchasyncrunBlocking,用于创建和启动协程。
    • launch 用于启动一个新的协程,它不返回结果。
    • async 用于启动一个新的协程,并返回一个 Deferred 对象,可以通过调用其 await() 方法来获取结果。
    • runBlocking 用于在当前线程上启动一个新的协程,并阻塞当前线程直到协程完成。
  3. 协程上下文(Coroutine Context)

    • 协程上下文是一个包含协程执行环境的集合,例如线程、协程调度器(CoroutineDispatcher)、协程名称等。
    • 协程调度器负责确定协程应该在哪个线程或线程池上执行。
  4. 协程调度器(Coroutine Dispatchers)

    • 协程调度器负责协程的线程调度。Kotlin 提供了多个预定义的调度器,如 Dispatchers.Main(主线程)、Dispatchers.IO(用于IO操作)、Dispatchers.Default(默认的线程池)和 Dispatchers.Unconfined(不限制协程的线程)。
    • 开发者可以自定义调度器,以满足特定的并发需求。
  5. Continuation 对象

    • 当一个挂起函数被调用时,它会返回一个 Continuation 对象。这个对象包含了恢复协程执行的代码和状态。
    • 挂起函数通过调用 Continuation 对象的 resume 方法来恢复协程的执行,并可以传递结果值。
  6. 编译器支持

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

现代开发不用了,可以考虑使用 suspendCoroutinesuspendCancellableCoroutine 协程构建函数,可以将异步代码转换为协程的同步代码

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

    }
}

suspendCoroutinesuspendCancellableCoroutine 的区别就是后者在协程处理完成(例如 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,有了这把利器,嘿嘿嘿。

相关推荐
xlsw_2 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹3 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭4 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫4 小时前
泛型(2)
java
超爱吃士力架4 小时前
邀请逻辑
java·linux·后端
南宫生4 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石4 小时前
12/21java基础
java
拭心4 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
李小白664 小时前
Spring MVC(上)
java·spring·mvc
GoodStudyAndDayDayUp4 小时前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea