如何使用 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,有了这把利器,嘿嘿嘿。

相关推荐
逊嘘11 分钟前
【Java语言】抽象类与接口
java·开发语言·jvm
帅得不敢出门16 分钟前
Gradle命令编译Android Studio工程项目并签名
android·ide·android studio·gradlew
morris13118 分钟前
【SpringBoot】Xss的常见攻击方式与防御手段
java·spring boot·xss·csp
七星静香43 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员44 分钟前
java导出word文件(手绘)
java·开发语言·word
ZHOUPUYU44 分钟前
IntelliJ IDEA超详细下载安装教程(附安装包)
java·ide·intellij-idea
stewie61 小时前
在IDEA中使用Git
java·git
problc1 小时前
Flutter中文字体设置指南:打造个性化的应用体验
android·javascript·flutter
Elaine2023911 小时前
06 网络编程基础
java·网络
G丶AEOM1 小时前
分布式——BASE理论
java·分布式·八股