kotlin之协程(二),Kotlin协程是什么、挂起是什么、挂起的非阻塞式

目录

kotlin之协程(一),线程,进程,协程,协程可以替换线程吗?
kotlin之协程(二),Kotlin协程是什么、挂起是什么、挂起的非阻塞式
kotlin之协程(三),开始创建协程,launch,withContext
kotlin之协程(四),协程的核心关键字suspend
kotlin之协程(五),launch 函数以及协程的取消与超时
kotlin之协程(六),协程中的 async和launch的区别以及runBlocking
kotlin之协程(七),协程中relay、yield 区别

前言

这里推荐一下"扔物线"的的网站,看了他讲解让我对kotlin的协程有了更深刻的了解,而我接下来的文章来自"扔物线"的讲述协程

传送地址: 扔物线-协程

扔物线
码上开学创始人、项目管理人、内容模块规划者和视频内容作者。
Android GDE( Google 认证 Android 开发专家),前 Flipboard Android 工程师。
GitHub 全球 Java 排名第 92 位,在 GitHub 上有 6.6k followers 和 9.9k stars。
个人的 Android 开源库 MaterialEditText 被全球多个项目引用,其中包括在全球拥有 5 亿用户的新闻阅读软件 Flipboard 。
曾多次在 Google Developer Group Beijing 线下分享会中担任 Android 部分的讲师。

关于kotlin中的协程概念

首先上篇文章中说到的协程并不是 Kotlin 提出来的新概念,很多语言都有这个特性
而网上对协程的概念有很多描述,上片文章里面就收集了很多对于协程的描述,但是协程本身没有一个明确的定义

抛开其他语言,对于kotlin而言

协程就是 Kotlin 提供的一套线程封装的 API(线程框架),但并不是说协程就是为线程而生的。kotlin中的协程类似于 Java 自带的 Executor 系列 API 或者 Android 的 Handler 系列 API。

不过,我们学习 Kotlin 中的协程,一开始确实可以从线程控制的角度来切入。因为在 Kotlin 中,协程的一个典型的使用场景就是线程控制。就像 Java 中的 Executor 和 Android 中的 AsyncTask,Kotlin 中的协程也有对 Thread API 的封装,让我们可以在写代码时,不用关注多线程就能够很方便地写出并发操作。

从 Android 开发者的角度去理解协程和线程的关系:

我们所有的代码都是跑在线程中的,而线程是跑在进程中的。
协程没有直接和操作系统关联,但它不是空中楼阁,它也是跑在线程中的,可以是单线程,也可以是多线程。
单线程中的协程总的执行时间并不会比不用协程少。

kotlin中使用协程进行耗时操作的本质是什么

Android 系统上,如果在主线程进行网络请求,会抛出 NetworkOnMainThreadException,对于在主线程上的协程也不例外,这种场景使用协程还是要切线程的。

kotlin中协程的特点

而对于kotlin中协程最重要的一点特点是,是可以用同步的方式写出异步的代码

下面是kotlin中协程一种写法:

coroutineScope.launch(Dispatchers.Main) {   // 在主线程开启协程
    val user = api.getUser() // IO 线程执行网络请求
    nameTv.text = user.name  // 主线程更新 UI
}

而通过 Java 实现以上逻辑,我们通常需要这样写:

api.getUser(new Callback<User>() {
    @Override
    public void success(User user) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                nameTv.setText(user.name);
            }
        })
    }
    
    @Override
    public void failure(Exception e) {
        ...
    }
});

Java的这种写法回调打乱了代码的顺序性,使得阅读非常的难受

对于回调式的写法,如果并发场景再复杂一些,代码的嵌套可能会更多,这样的话维护起来就非常麻烦。但如果你使用了 Kotlin 协程,多层网络请求只需要这么写:

coroutineScope.launch(Dispatchers.Main) {       // 开始协程:主线程
    val token = api.getToken()                  // 网络请求:IO 线程
    val user = api.getUser(token)               // 网络请求:IO 线程
    nameTv.text = user.name                     // 更新 UI:主线程
}

可以看到,即便是比较复杂的并行网络请求,也能够通过协程写出结构清晰的代码

协程中挂起的本质

协程中「挂起」的对象到底是什么?挂起线程,还是挂起函数?都不对,我们挂起的对象是协程

启动一个协程可以使用 launch 或者 async 函数,协程其实就是这两个函数中闭包的代码块。

launch ,async 或者其他函数创建的协程,在执行到某一个 suspend 函数的时候,这个协程会被「suspend」,也就是被挂起。

注意:关于launch async suspend 这些概念我们后续会讲大家先知道这是协程的一个内容

那此时又是从哪里挂起?从当前线程挂起。换句话说,就是这个协程从正在执行它的线程上脱离。

suspend 是有暂停的意思,但我们在协程中应该理解为:当线程执行到协程的 suspend 函数的时候,暂时不继续执行协程代码了。

我们先让时间静止,然后兵分两路,分别看看这两个互相脱离的线程和协程接下来将会发生什么事情:

线程:

前面我们提到,挂起会让协程从正在执行它的线程上脱离,具体到代码其实是:

协程的代码块中,线程执行到了 suspend 函数这里的时候,就暂时不再执行剩余的协程代码,跳出协程的代码块。

那线程接下来会做什么呢?

如果它是一个后台线程:

要么无事可做,被系统回收
要么继续执行别的后台任务
跟 Java 线程池里的线程在工作结束之后是完全一样的:回收或者再利用。

如果这个线程它是 Android 的主线程,那它接下来就会继续回去工作:也就是一秒钟 60 次的界面刷新任务。

一个常见的场景是,获取一个图片,然后显示出来:

// 主线程中
GlobalScope.launch(Dispatchers.Main) {
  val image = suspendingGetImage(imageId)  // 获取图片
  avatarIv.setImageBitmap(image)           // 显示出来
}

suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {
  ...
}

这段执行在主线程的协程,它实质上会往你的主线程 post 一个 Runnable,这个 Runnable 就是你的协程代码:

handler.post {
  val image = suspendingGetImage(imageId)
  avatarIv.setImageBitmap(image)
}

当这个协程被挂起的时候,就是主线程 post 的这个 Runnable 提前结束,然后继续执行它界面刷新的任务。

关于线程,我们就看完了。
这个时候你可能会有一个疑问,那 launch 包裹的剩下代码怎么办?

所以接下来,我们来看看协程这一边。

协程:

线程的代码在到达 suspend 函数的时候被掐断,接下来协程会从这个 suspend 函数开始继续往下执行,不过是在指定的线程

谁指定的?是 suspend 函数指定的,比如我们这个例子中,函数内部的 withContext 传入的 Dispatchers.IO 所指定的 IO 线程。

Dispatchers 调度器,它可以将协程限制在一个特定的线程执行,或者将它分派到一个线程池,或者让它不受限制地运行,关于 Dispatchers 这里先不展开了。

那我们平日里常用到的调度器有哪些?

常用的 Dispatchers ,有以下三种:

  • Dispatchers.Main:Android 中的主线程
  • Dispatchers.IO:针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求
  • Dispatchers.Default:适合 CPU 密集型的任务,比如计算

回到我们的协程,它从 suspend 函数开始脱离启动它的线程,继续执行在 Dispatchers 所指定的 IO 线程。

紧接着在 suspend 函数执行完成之后,协程为我们做的最爽的事就来了:会自动帮我们把线程再切回来

这个「切回来」是什么意思?

我们的协程原本是运行在主线程的,当代码遇到 suspend 函数的时候,发生线程切换,根据 Dispatchers 切换到了 IO 线程;

当这个函数执行完毕后,线程又切了回来,「切回来」也就是协程会帮我再 post 一个 Runnable,让我剩下的代码继续回到主线程去执行。

我们从线程和协程的两个角度都分析完成后,终于可以对协程的「挂起」suspend 做一个解释:

协程在执行到有 suspend 标记的函数的时候,会被 suspend 也就是被挂起,而所谓的被挂起,就是切个线程;

不过区别在于,挂起函数在执行完成之后,协程会重新切回它原先的线程

再简单来讲,在 Kotlin 中所谓的挂起,就是一个稍后会被自动切回来的线程调度操作

这个「切回来」的动作,在 Kotlin 里叫做 resume,恢复。

通过刚才的分析我们知道:挂起之后是需要恢复。

而恢复这个功能是协程的,如果你不在协程里面调用,恢复这个功能没法实现,所以也就回答了这个问题:为什么挂起函数必须在协程或者另一个挂起函数里被调用。

再细想下这个逻辑:一个挂起函数要么在协程里被调用,要么在另一个挂起函数里被调用,那么它其实直接或者间接地,总是会在一个协程里被调用的。

所以,要求 suspend 函数只能在协程里或者另一个 suspend 函数里被调用,还是为了要让协程能够在 suspend 函数切换线程之后再切回

通过刚才的分析我们知道:挂起之后是需要恢复。

而恢复这个功能是协程的,如果你不在协程里面调用,恢复这个功能没法实现,所以也就回答了这个问题:为什么挂起函数必须在协程或者另一个挂起函数里被调用。

再细想下这个逻辑:一个挂起函数要么在协程里被调用,要么在另一个挂起函数里被调用,那么它其实直接或者间接地,总是会在一个协程里被调用的。

所以,要求 suspend 函数只能在协程里或者另一个 suspend 函数里被调用,还是为了要让协程能够在 suspend 函数切换线程之后再切回来。

什么是「非阻塞式挂起」

非阻塞式是相对阻塞式而言的。

编程语言中的很多概念其实都来源于生活,就像脱口秀的段子一样。

线程阻塞很好理解,现实中的例子就是交通堵塞,它的核心有 3 点:

  • 前面有障碍物,你过不去(线程卡了)
  • 需要等障碍物清除后才能过去(耗时任务结束)
  • 除非你绕道而行(切到别的线程)

从语义上理解「非阻塞式挂起」,讲的是「非阻塞式」这个是挂起的一个特点,也就是说,协程的挂起,就是非阻塞式的,协程是不讲「阻塞式的挂起」的概念的。

我们讲「非阻塞式挂起」,其实它有几个前提:并没有限定在一个线程里说这件事,因为挂起这件事,本来就是涉及到多线程。

就像视频里讲的,阻塞不阻塞,都是针对单线程讲的,一旦切了线程,肯定是非阻塞的,你都跑到别的线程了,之前的线程就自由了,可以继续做别的事情了。

所以「非阻塞式挂起」,其实就是在讲协程在挂起的同时切线程这件事情。

为什么要讲非阻塞式挂起

「非阻塞式挂起」和第二篇的「挂起要切线程」是同一件事情,那还有讲的必要吗?

是有的。因为它在写法上和单线程的阻塞式是一样的。

协程只是在写法上「看起来阻塞」,其实是「非阻塞」的,因为在协程里面它做了很多工作,其中有一个就是帮我们切线程。

之前说的挂起,重点是说切线程先切过去,然后再切回来。

而这里的非阻塞式,重点是说线程虽然会切,但写法上和普通的单线程差不多。

让我们来看看下面的例子:

main {
    GlobalScope.launch(Dispatchers.Main) {
        // 耗时操作
        val user = suspendingRequestUser()
        updateView(user)
    }
    
    private suspend fun suspendingRequestUser() : User = withContext(Dispatchers.IO) {
        api.requestUser()
    }
}

从上面的例子可以看到,耗时操作和更新 UI 的逻辑像写单线程一样放在了一起,只是在外面包了一层协程。

而正是这个协程解决了原来我们单线程写法会卡线程这件事。

阻塞的本质

首先,所有的代码本质上都是阻塞式的,而只有比较耗时的代码才会导致人类可感知的等待,比如在主线程上做一个耗时 50 ms 的操作会导致界面卡掉几帧,这种是我们人眼能观察出来的,而这就是我们通常意义所说的「阻塞」。

举个例子,当你开发的 app 在性能好的手机上很流畅,在性能差的老手机上会卡顿,就是在说同一行代码执行的时间不一样。

视频中讲了一个网络 IO 的例子,IO 阻塞更多是反映在「等」这件事情上,它的性能瓶颈是和网络的数据交换,你切多少个线程都没用,该花的时间一点都少不了。

而这跟协程半毛钱关系没有,切线程解决不了的事情,协程也解决不了

总结

关于这边文章的标题协程是什么、挂起是什么、挂起的非阻塞式可以做下面的总结

  • 协程是什么

协程就是切线程;

  • 挂起是什么

挂起就是可以自动切回来的切线程;

  • 挂起的非阻塞式

挂起的非阻塞式指的是它能用看起来阻塞的代码写出非阻塞的操作。

(每天学习一点点.每天进步一点点,分享不宜路过点个赞呀)

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