使用 Kotlin 协程思考体会
记录下我在开发过程中使用 Kotlin 协程的一些思考和体会。现代编程语言越来越倾向于让异步操作写起来像同步代码一样 ------ 简洁、直观、逻辑清晰。在使用 Kotlin 协程时最深的感受:它让复杂的异步流程变得优雅而自然,开发体验大幅提升。
(最近用rust使用async/wait那套机制也有类似的感受)
Kotlin协程对比传统异步编程
协程写法
kotlin
fun main() = runBlocking {
val result = getDataFromNet()
updateUI(result)
}
suspend fun getDataFromNet(): String {
return withContext(Dispatchers.IO) {
api.getData()
}
}
传统写法
kotlin
fun main() {
getDataFromNet(object: Callback<String> {
override fun onSuccess(result: String) {
updateUI(result)
}
})
}
fun getDataFromNet(callback: Callback<String>) {
api.getDataAsync(callback)
}
-
传统异步写法: 调用完后并不能立刻获得结果,而是通过回调接口来处理。这种方式的代码结构容易被打断,逻辑跳跃感较强,尤其在嵌套和多层回调的情况下尤为明显。
-
Kotlin 协程: 通过挂起机制,能够用接近同步代码的方式来处理异步操作。这样,代码结构变得更加直观,逻辑更清晰,避免了回调的层层嵌套。
在涉及线程切换和多重嵌套时,代码结构往往变得混乱,极易引发"回调地狱",所以会出现RxJava这样的框架来管理回调。
了解协程的逻辑
kotlin
fun main() {
val coroutine = CoroutineScope(Dispatchers.Unconfined)
coroutine.launch {
println("Thread = ${Thread.currentThread()}, 1, ${System.currentTimeMillis()}")
syncDelay(2000)
println("Thread = ${Thread.currentThread()}, 2, ${System.currentTimeMillis()}")
}
println("Thread = ${Thread.currentThread()}, 3, ${System.currentTimeMillis()}")
}
fun syncDelay(timeMillis: Long) {
val cur = System.currentTimeMillis()
while (true) {
if (System.currentTimeMillis() > cur + timeMillis) {
break
}
}
}
CoroutineScope 可以理解为开启了一个轻量级的"任务容器",在这个容器中可以运行挂起函数等协程相关的功能。
CoroutineScope的构造函数可以传入一个调度器用于设置这个容器的默认线程。 这里默认使用当前main()函数的主线程作为容器的默认线程。
coroutine.launch { } 可以理解为启动容器内的一个片段。现在这个片段也是运行在主线程的。
程序运行结果:
shell
Thread = Thread[main,5,main], 1, 1744356837002
Thread = Thread[main,5,main], 2, 1744356839010
Thread = Thread[main,5,main], 3, 1744356839010
可以看到当片段运行在主线程,看起来就是顺序执行的结果。
如果我们把耗时操作放在io线程呢?
kotlin
fun main() {
val coroutine = CoroutineScope(Dispatchers.Unconfined)
coroutine.launch {
println("Thread = ${Thread.currentThread()}, 1, ${System.currentTimeMillis()}")
asyncDelay(2000)
println("Thread = ${Thread.currentThread()}, 2, ${System.currentTimeMillis()}")
}
println("Thread = ${Thread.currentThread()}, 3, ${System.currentTimeMillis()}")
}
suspend fun asyncDelay(timeMillis: Long) {
return withContext(Dispatchers.IO) {
val cur = System.currentTimeMillis()
while (true) {
if (System.currentTimeMillis() > cur + timeMillis) {
break
}
}
}
}
程序运行结果:
shell
Thread = Thread[main,5,main], 1, 1744361542270
Thread = Thread[main,5,main], 3, 1744361542283
可以看到程序好像跳过了片段内asyncDelay()及之后的函数,直接打印了3。 最终没有看到2的结果是因为程序提前结束了。
这里引出一个重要的关键字 suspend, 它定义了可以挂起的操作。
意思是说当协程遇到挂起函数时,会暂停执行片段内的程序,等耗时函数执行异步执行完后恢复重新执行。
可以再看一个有趣的例子,如果suspend里不切换线程,而是用原来的线程呢? 再syncDelay()方法上添加suspend。
kotlin
fun main() {
// 使用当前线程的域
val coroutine = CoroutineScope(Dispatchers.Unconfined)
coroutine.launch {
println("Thread = ${Thread.currentThread()}, 1, ${System.currentTimeMillis()}")
syncDelay(2000)
println("Thread = ${Thread.currentThread()}, 2, ${System.currentTimeMillis()}")
}
println("Thread = ${Thread.currentThread()}, 3, ${System.currentTimeMillis()}")
}
suspend fun syncDelay(timeMillis: Long) {
val cur = System.currentTimeMillis()
while (true) {
if (System.currentTimeMillis() > cur + timeMillis) {
break
}
}
}
程序运行结果:
shell
Thread = Thread[main,5,main], 1, 1744361660272
Thread = Thread[main,5,main], 2, 1744361662280
Thread = Thread[main,5,main], 3, 1744361662280
和第一次结果一样,它会堵塞主线程,因为它使用了主线程执行耗时任务。
所以添加了suspend关键字并不会帮我们自动切换线程,它只是一个标识。告诉编译器这是一个挂起函数。
在launch{}内使用多个launch{}会怎么样?
kotlin
fun main() {
// 使用当前线程的域
val coroutine = CoroutineScope(Dispatchers.Unconfined)
coroutine.launch {
launch {
println("Thread = ${Thread.currentThread()}, 1, ${System.currentTimeMillis()}")
asyncDelay(2000)
println("Thread = ${Thread.currentThread()}, 2, ${System.currentTimeMillis()}")
}
launch {
println("Thread = ${Thread.currentThread()}, 3, ${System.currentTimeMillis()}")
asyncDelay(2000)
println("Thread = ${Thread.currentThread()}, 4, ${System.currentTimeMillis()}")
}
}
Thread.sleep(3000) // 防止程序提前结束
println("Thread = ${Thread.currentThread()}, 5, ${System.currentTimeMillis()}")
}
程序运行结果:
shell
Thread = Thread[main,5,main], 1, 1744362049871
Thread = Thread[main,5,main], 3, 1744362049884
Thread = Thread[DefaultDispatcher-worker-1,5,main], 2, 1744362051885
Thread = Thread[DefaultDispatcher-worker-3,5,main], 4, 1744362051885
Thread = Thread[main,5,main], 5, 1744362052885
内部的1,3像并发一样在执行。
意味着可以在协程内写并发程序。
总结
最后,很浅显的理解是,协程就像是开辟了一个新的"任务容器",你可以通过 launch {} 把代码块放进去执行,它的角色有点像线程中的 Runnable ------ 定义具体要做的事。但不同的是,协程可以用 suspend 函数进行"挂起",也就是中断当前任务的执行,等到合适的时候再恢复继续执行,而且整个过程不会阻塞线程。
从上可以看出协程支持灵活的线程切换,可以通过 withContext(...) 轻松地在不同线程间调度任务。