kotlin协程的简述和使用

为什么会有协程 && 什么是协程

当我们最初学习程序之时,我们书写代码、使用指令执行,完成逻辑链条的前后关系。代码执行到哪,逻辑就走到哪。但问题随之出现,有些过程并不是能立即得到结果的,监听按钮或者一些耗时操作如IO操作等,程序为了等待结果就会阻塞。在一些应用场景之下,我们常常会使用异步api,通过一些回调函数操作来完成异步任务。
<div align=center><img src="https://s1.ax1x.com/2022/09/26/xV3LFg.png" width="40%" /></div>

但是异步回调也有本身的问题,第一是,原本的统一的同步逻辑被拆分成几个阶段,造成代码可读性不好。第二是,在复杂的应用场景下,可能造成十分难受的回调地狱。以及难以debug的问题。

// JavaScript展示地狱回调
setTimeout(function () {  // 第一层
    console.log('第一层'); // 等3秒打印,再执行下一个回调函数
    setTimeout(function () {  // 第二层
        console.log('第二层'); // 等2秒打印,再执行下一个回调函数
        setTimeout(function () {   // 第三层
            console.log('第三层'); // 等1秒打印
        }, 1000)
    }, 2000)
}, 3000)

协程则应运而生。协程就是协作式多任务,协程本身是一种任务调度机制,也可以说是一种编程设计思想并不限定语言(在1958年就被发明,并用于构建汇编程序)。协程可以使用同步的代码逻辑流去操作异步的控制流。并且不会导致在操作系统的线程阻塞。相对于线程之间的切换,协程是更轻量级的。对于线程的操作是调用了操作系统的功能,而启用协程则是编程语言来完成,所以协程也被称为用户态线程。

协程为并发而生,线程在CPU多核之下,已然能完成并行。如图所示,并发本身是对CPU时间片的争夺。而对于异步任务进行线程切换,这才是协程能做的事。一个线程可以有多个等待执行的协程, 它们不像多线程争抢cpu那样, 它们是排队执行。

下图为CPU时间片段对于串行、并行、并发的线程处理的区别。
<div align=center><img src="https://s1.ax1x.com/2022/09/25/xEV9BR.jpg" width="40%" /></div>

协程对于异步任务是主动让出而不是抢占的多任务,突出是主动让出;而线程则是抢占式的多任务,突出被动抢占。

抢占是一个相对低效的操作,“打断”这种操作不是那么容易做的,操作系统级别上之所以需要抢占是为了避免任务占着CPU不走。但在你自己知根知底的代码还要去抢占,这基本上就是牺牲性能的操作。所以使用协程避免抢占可以提高性能。

部分语言对于协程的支持

  • go语言的协程,直接在语法层面支持协程。解决了服务端开发中,IO密集型任务,并发性能程序过于复杂的痛点。go可以很优雅地进行高并发场景的开发。go语言的协程叫 Goroutines,从英文拼写就知道它和 Coroutines 还是有些差别的(设计思想上是有关系的),不然Kotlin的协程完全可以叫 Koroutines。
  • 之前Java对于协程而言,可以使用NIO(new IO)和一些多线程api进行操作,能模拟出一定的协程的效果,但实际开发方面还是过于麻烦,也许go语言在服务器端异军突起,在协程并发方面Java落于下风不无关系。
  • 2022年9月份,Oracle正式发布了最新版本的Java19和对应的Java虚拟线程的特性。是为帮助提高大型服务器的应用性能。Java虚拟线程在设计思想上,就是轻量级线程。值得关注的是,这次改动对于Java各个Api的改动很少,并没有太多新语法。Java19的虚拟线程是预览特性,很可能在Java21才会成为正式特性。
  • 很多语言都有自己的协程(虚拟线程)的技术,除了Go、Java,还有C#、Erlang、Lua等等。

kotlin协程

协程概念本身与线程概念是同一级别的东西。

在kotlin-JVM、Android平台中,对于协程的运用本质成为了切换线程,在性能上本身不会比优化后的线程池强。这是kotlin-JVM语言对于协程的实现。甚至在其他平台,kotlin-native、kotlin-javascript上,协程的实现的方法都完全不一样。在这个基础上,可以说,协程未必等于切线程,而未必就不能强于线程池。只是kotlin-JVM的协程实现就是基于切换线程。

kotlin协程写法

kotlin协程的写法和运用方案很多,有:runBlocking顶层函数;GlobalScope(CoroutineScope)单例对象调用launch开启协程;创建一个 CoroutineScope 对象开启协程等等。

这里我主要讲比较简单的开启协程的方法。

CoroutineScope(Dispatchers.Main).launch {
    // your code
    
    withContext(Dispatchers.IO) {
        // your blocking code
    }

    // your code

    withContext(Dispatchers.IO) {
        // your blocking code
    }
    
    // your code
    
}

如上的写法就是在main线程中进行异步操作,将阻塞和耗时操作放入withContext的切换线程的代码块里,kotlin程序就自然帮我们完成了线程之间的切换,以同步的写法完成异步的事情,这就是协程。

如下的代码是实现一个计时器更新主线程button的文字的功能,是kotlin使用协程对比使用线程池,kotlin-JVM协程在写法和性能上不一定更优秀,这些都是设计思想和方案选择的碰撞和抉择。如下代码,进行了简单的时间增加并在Android的UI线程更新的逻辑。

private fun useCoroutines() {
    CoroutineScope(Dispatchers.Main).launch {
        buttonCount.isEnabled = false
        var count = 0
        while (count < 10) {
            withContext(Dispatchers.IO) {
                delay(SECOND_DURATION)
                count++
            }
            buttonCount.text = String.format(
                Locale.getDefault(),
                "%d",
                count
            )
        }
        buttonCount.isEnabled = true
    }
}
private fun useExecutor() {
    Executors.newSingleThreadExecutor().execute {
        var count = 0
        while (count < 10) {
            SystemClock.sleep(SECOND_DURATION)
            count++
            val finalCount = count
            runOnUiThread {
                buttonCount.text = String.format(
                    Locale.getDefault(),
                    "%d",
                    finalCount
                )
            }
        }
        runOnUiThread { buttonCount.isEnabled = true }
    }
}

suspend关键字

suspend 是 Kotlin 协程最核心的关键字之一。官方解释是,代码执行到 suspend 函数的时候会『挂起』,并且这个『挂起』是非阻塞式的,它不会阻塞你当前的线程。

什么是挂起函数

suspend关键字修饰的函数叫做挂起函数,挂起函数只能在协程体内或者其他挂起函数内使用。协程内部挂起函数的调用处被称为挂起点,也就是Android Studio代码左边会出现的一个箭头加一个绿色波浪线的标志。

网上寻到的gif例子,具体挂起函数演示如下:

<div align=center><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/60453cfebece44779b6581aefef14284~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.image" width="40%" /></div>

getUserInfo()、getFriendList(user)、getFeedList(friendList)三个函数都是挂起函数。内部必然为 suspend修饰的挂起函数。例如getUserInfo函数可为下内容:

suspend fun getUserInfo(): String {
    withContext(Dispatchers.IO) { // 切换到IO线程
        delay(1000L) // 延迟1s
    }
    return "content"
}

挂起的对象是协程,挂起的本质“就是这个协程从正在执行它的线程上脱离”。注意,这里不是这个协程停下来,而是脱离,从当前线程脱离,比如上文的代码,就是从主线程脱离,去IO或工作线程上进行处理。紧接着在 suspend 函数执行完成之后,协程为我们做的最爽的事就来了:会自动帮我们把线程再切回来。如上文所示,我们代码本身在主线程允许,当协程切走的函数执行完毕,协程会帮助我们post一个Runnable,让我们剩下的代码和信息继续回到之前的线程去运行。

协程scope

  • 值得关注的是,kotlin的协程它也有自己的scope。比如使用api GlobalScope.launch 不如使用 api CoroutineScope.launch,原因就是GlobalScope.launch的协程作用域不受限制, 即除非主进程退出, 否则只要该协程不结束就会占用资源,因为是全局的scope。这导致了如果协程的执行体中出现异常协程仍会占用资源而非释放. 最差的情况下有可能反复调用导致设备资源被占满宕机,这也就是内存溢出。

参考文献

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,039评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,426评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,417评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,868评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,892评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,692评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,416评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,326评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,782评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,957评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,102评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,790评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,442评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,996评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,113评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,332评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,044评论 2 355

推荐阅读更多精彩内容