Kotlin 使用协程编写高效的并发程序

协程和线程有点类似,可以简单的理解成一种轻量级的线程。协程可以仅在编程语言层面实现不同协程之间的切换,从而大大提升并发编程的运行效率。协程允许在单线程模式下模拟多线程的编程效果,代码执行时的挂起与恢复完全是由编程语言来控制的,和操作系统无关。

协程的基本用法

Kotlin 协程是以官方扩展库的形式进行支持的,需要先添加依赖才能使用:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'

GlobalScope.launch 函数可以创建一个协程的作用域,这样传递给 launch 函数的代码块就是在协程中运行的了:

GlobalScope.launch {
println("codes run in coroutine scope")
delay(1500)
}

GlobalScope.launch 函数创建的永远是顶层协程。管理成本比较高,在实际项目中也不太常用。

delay() 函数可以让当前协程延迟指定时间后再运行。delay() 函数是一个非阻塞式的挂起函数,它只会挂起当前协程,并不会影响其他协程的运行,只能在协程的作用域或其他挂起函数中调用。Thread.sleep() 方法会阻塞当前的线程,这样运行在该线程下的所有协程都会被阻塞。

runBlocking{
println("codes run in coroutine scope)
}

runBlocking 函数同样会创建一个协程的作用域,但是它可以保证协程作用域内的所有代码和子协程没有全部执行完之前一直阻塞当前线程。如果恰好在主线程中调用,有可能导致界面卡死。runBlocking 函数通常只应该在测试环境下使用,在正式环境中使用容易产生一些性能上的问题。

launch 函数必须在协程的作用域中才能调用,会在当前协程的作用域下创建子协程。子协程的特点是如果外层作用域的协程结束了,该作用域下的所有子线程也会一同结束。

runBlocking{
launch{
println("launch1")
}
launch{
println("launch2")
}
}

suspend 关键字可以将任意函数声明成挂起函数,挂起函数之间可以相互调用。

coroutineScope 函数是一个挂起函数,可以在其他挂起函数中调用,它会继承外部的协程作用域并创建一个子作用域。

suspend fun printDot() = coroutineScope {
launch {
println(".")
}
}

coroutineScope 函数可以保证其作用域内的所有代码和子协程在全部执行完之前,外部的协程会一直被挂起,只有当它作用域内的所有代码和子协程都执行完毕之后,coroutineScope 函数之后的代码才能得到运行。coroutineScope 函数只会阻塞当前协程,既不影响其他协程,也不影响任何线程,因此不会造成任何性能上的问题。

更多的作用域构建器

实际项目中比较常用的写法:

val job= Job()
val scope= CoroutineScope(job)
scope.launch {
// 处理具体的逻辑
}
job.cancel()

先创建了一个 Job 对象,然后传入 CoroutineScope() 函数中,所有调用 CoroutineScopelaunch 函数所创建的协程都会被关联在 Job 对象的作用域下面,只需要调用一次 cancel() 方法就可以将同一作用域内的所有协程全部取消,从而大大降低协程管理的成本。

launch 函数只能用于执行一段逻辑却不能获取执行结果,因为它的返回值永远是一个 Job 对象。使用 async 函数可以创建一个协程并获取它的执行结果。

async 函数必须在协程的作用域中才能调用,它会创建一个新的子协程并返回一个 Deferred 对象,如果想要获取 async 代码块的执行结果,只需要调用 Deferred 对象的 await() 方法即可:

runBlocking{
val result1 = async {
5 + 5
}.await()
val result2 = async {
4 + 6
}.await()
println("result is ${result1 + result2}.")
}

调用了 async 函数后,代码块中的代码会立刻执行,当调用 await() 方法时,如果代码块中的代码还没有执行完,那么 await() 方法将会将当前协程阻塞住,直到可以获得 async 函数的执行结果。以上代码串行执行。

runBlocking{
val deferred1 = async {
5 + 5
}
val deferred2 = async {
4 + 6
}
println("result is ${deferred1.await() + deferred2.await()}.")
}

以上代码并行执行。

withContext() 函数是一个挂起函数:

runBlocking {
val result = withContext(Dispatchers.Default) {
5 + 5
}
println(result)
}

调用 withContext() 函数后会立即执行代码块中的代码,同时将当前协程阻塞住,当代码块中的代码全部执行完之后,会将最后一行的执行结果作为 withContext() 函数的返回值进行返回。withContext() 函数强制要求指定线程参数,其他几个函数是可选的(coroutineScope 函数不指定)。

常用的线程参数有以下 3 种:

  • Dispatchers.Default:使用一种默认低并发的线程策略,适合计算密集型任务

  • Dispatchers.IO:使用较高并发的线程策略,针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,如:读写文件、操作数据库以及网络请求

  • Dispatchers.Main:不会开启子线程,在 Android 主线程种执行。

使用协程简化回调的写法

suspendCoroutine 函数必须在协程作用域或挂起函数中才能使用,它接收一个 Lambda 表达式参数,主要作用是将当前协程立即挂起,然后在一个普通线程种执行 Lambda 表达式中的代码。Lambda 表达式的参数列表上会传入一个 Continuation 参数,调用它的 resume() 方法或 resumeWithException() 可以让协程恢复执行。

suspend fun <T> Call<T>.await(): T {
return suspendCoroutine { continuation ->
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
val body = response.body()
if (body != null) continuation.resume(body)
else continuation.resumeWithException(RuntimeException("response body is null"))
}

override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}

参考