💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## 参考文章 [Kotlin 的协程用力瞥一眼 - 学不会协程?很可能因为你看过的教程都是错的](https://kaixue.io/kotlin-coroutines-1/) [Kotlin 协程的挂起好神奇好难懂?今天我把它的皮给扒了](https://kaixue.io/kotlin-coroutines-2/) [到底什么是「非阻塞式」挂起?协程真的更轻量级吗?](https://kaixue.io/kotlin-coroutines-3/) # 初识协程 ## 什么是协程? 协程原本是一个跟线程非常类似的用于处理多任务的概念,但在Kotlin,协程其实就是一套**由Kotlin官方提供的线程API**,就像Java的Executor和Android的AsyncTask,Kotlin协程也对Thread相关的API做了一套封装,让我们不用过多关心线程也可以方便地写出并发操作。 ## 协程的优势 既然协程类似于Java的Executor和Android的AsyncTask,那还要协程干嘛呢? 协程本质上和那些其他的线程API一样方便,但是它借助了kotlin的语言优势,所以它比那些基于Java的方案会更方便一点,最重要的是,它可以用看起来同步的代码方式来写出异步代码,即**非阻塞式挂起**。 协程最基本的功能就是并发,多线程,用协程你可以把任务切到后台执行。 比如 ~~~kotlin coroutineScope.launch(Dispatchers.IO) { ... } ~~~ ~~~kotlin coroutineScope.launch(Dispatchers.Main) { ... } ~~~ 这种写法很简单,但是它并不能算是协程相对于直接使用Thread的优势,因为Kotlin已经专门添加了一个函数,来简化对Thread的直接使用,而kotlin协程最大的好处就是你可以把运行在不同线程的代码写在同一个代码块里 ~~~kotlin coroutineScope.launch(Dispatchers.Main) { // 开始协程:主线程 val token = api.getToken() // 网络请求:IO 线程(后台线程) val user = api.getUser(token) // 网络请求:IO 线程(后台线程) nameTv.text = user.name // 更新 UI:主线程 } ~~~ 上下两行代码线程切走再切回来,这是Java永远做不到的。不过这个说实话,差别并不大,毕竟我们写回调早就写熟练了,即便是回调地狱,对于习惯的人来说倒也还好,毕竟所谓的回调地狱一般也就二三层。但是,这种不大的差别其实也蛮大的,协程改变了并发任务的操作难度。消除了回调,那么多线程协作任务的难度直接就抹平了,没有了。这其实是质变的,而且这种质变也对我们的开发工作带来了质变。协程可以让我们轻松地写出复杂的并发代码,而且由于并发代码变得不难写,一些本来不可能实现的并发任务变得可能,甚至变得很简单,这些才是协程的优势所在。 ## 怎么用协程 ~~~kotlin launch(Dispatchers.IO) { val image = getImage(imageId) } ~~~ 这个launch函数,具体的含义就是我要创建一个新的协程,并在指定线程上运行它。这个被创建被运行的所谓协程是谁,就是你传给launch函数的那些代码,这一段连续代码就叫做一个协程。 ### 什么时候用协程? 当于需要并发任务,切线程或者指定线程的时候。 协程中却有一个很实用的函数:`withContext`。这个函数可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行。 ~~~kotlin coroutineScope.launch(Dispatchers.Main) { // 👈 在 UI 线程开始 val image = withContext(Dispatchers.IO) { // 👈 切换到 IO 线程,并在执行完成后切回 UI 线程 getImage(imageId) // 👈 将会运行在 IO 线程 } avatarIv.setImageBitmap(image) // 👈 回到 UI 线程更新 UI } ~~~ 这种写法看上去好像和刚才那种区别不大,但如果你需要频繁地进行线程切换,这种写法的优势就会体现出来。可以参考下面的对比: ~~~kotlin // 第一种写法 coroutineScope.launch(Dispatchers.IO) { ... launch(Dispatchers.Main){ ... launch(Dispatchers.IO) { ... launch(Dispatchers.Main) { ... } } } } // 通过第二种写法来实现相同的逻辑 coroutineScope.launch(Dispatchers.Main) { ... withContext(Dispatchers.IO) { ... } ... withContext(Dispatchers.IO) { ... } ... } ~~~ 由于可以"自动切回来",消除了并发代码在协作时的嵌套。由于消除了嵌套关系,我们甚至可以把`withContext`放进一个单独的函数里面: ~~~kotlin launch(Dispatchers.Main) { // 👈 在 UI 线程开始 val image = getImage(imageId) avatarIv.setImageBitmap(image) // 👈 执行结束后,自动切换回 UI 线程 } // 👇 fun getImage(imageId: Int) = withContext(Dispatchers.IO) { ... } ~~~ 这就是之前说的「用同步的方式写异步的代码」了。 不过如果只是这样写,编译器是会报错的: ~~~kotlin fun getImage(imageId: Int) = withContext(Dispatchers.IO) { // IDE 报错 Suspend function'withContext' should be called only from a coroutine or another suspend funcion } ~~~ 意思是说,`withContext`是一个`suspend`函数,它需要在协程或者是另一个`suspend`函数中调用。 `suspend`是 Kotlin 协程最核心的关键字,它的中文意思是「暂停」或者「可挂起」。如果你去看一些技术博客或官方文档的时候,大概可以了解到:「代码执行到`suspend`函数的时候会『挂起』,并且这个『挂起』是非阻塞式的,它不会阻塞你当前的线程。」 上面报错的代码,其实只需要在前面加一个`suspend`就能够编译通过: ~~~kotlin //👇 suspend fun getImage(imageId: Int) = withContext(Dispatchers.IO) { ... } ~~~ ## 总结 协程到底是什么?其实**它就是一个比较方便的线程框架**,网络上的什么像是线程,但又不是线程,它是用户态的,它是协作式的,这些云里雾里,晦涩难懂的标题式答案都不正确。 好处:方便 最大的好处:在它能够在同一个代码块里进行多次的线程切换 # Kotlin 协程的挂起