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

相关推荐
stevenzqzq1 小时前
kotlin扩展函数
android·开发语言·kotlin
Hello姜先森1 小时前
Kotlin日常使用函数记录
android·开发语言·kotlin
zhangphil1 小时前
Android Coil 3 Fetcher大批量Bitmap拼接成1张扁平宽图,Kotlin
android·kotlin
harry235day2 小时前
Compose 自定义转盘
kotlin·android jetpack
_一条咸鱼_2 小时前
大厂Android面试秘籍: Android Activity 状态保存与恢复机制(六)
android·面试·kotlin
_一条咸鱼_2 小时前
大厂Android面试秘籍:Activity 布局加载与视图管理(五)
android·面试·kotlin
_一条咸鱼_1 天前
Android大厂面试秘籍:Activity 启动与任务栈管理原理
android·面试·kotlin
_一条咸鱼_2 天前
Android 大厂面试秘籍:Hilt 框架的测试支持模块(八)
android·面试·kotlin
人生游戏牛马NPC1号2 天前
学习Android(一)
android·kotlin