使用 Kotlin 协程思考体会

使用 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(...) 轻松地在不同线程间调度任务。

相关推荐
移动开发者1号1 小时前
Fragment懒加载优化方案总结
android·kotlin
移动开发者1号1 小时前
Android Activity启动模式面试题
android·kotlin
alexhilton2 小时前
Jetpack Compose 中ViewModel的最佳实践
android·kotlin·android jetpack
猿小蔡-Cool5 小时前
Kotlin 中 Lambda 表达式的语法结构及简化推导
开发语言·windows·kotlin
wzj_what_why_how19 小时前
Kotlin JVM 注解详解
android·kotlin
雨白1 天前
Kotlin 标准函数 with, run, apply 与静态方法实现
kotlin
tangweiguo030519871 天前
Android全局网络监控最佳实践(Kotlin实现)
android·kotlin
移动开发者1号1 天前
Android后台服务保活方案对比分析
android·kotlin
移动开发者1号1 天前
ContentProvider URI匹配机制详解
android·kotlin
zhifanxu2 天前
android协程异步编程常用方法
android·开发语言·kotlin