速通协程,一步到位!

news2024/7/6 18:05:02

前言

协程的概念最核心的点就是一段程序能够被挂起,稍后在挂起的位置恢复,挂起和恢复是由使用者控制的。

数学模型

在一个线程的视角中,我们的程序是按照顺序来执行的,假设我们使用??????来描述一段程序的所有指令。那么协程就是该指令流中的一部分(我们让该协程也运行在当前线程上),我们假设它为????,此时 0 < i < j < n。协程中会存在挂起函数,假设挂起函数对应的指令为??,此时 i < k < j。

协程神奇的地方就在于,当指令从???执行到???的时候,它是顺序的,和正常没有区别。当指令从???执行到???的时候,也是和正常没有区别。当指令遇到???的时候,该程序就不往下执行了,而是跑去执行???了,这就是我们上面所描述的该协程挂起了,那么什么时候恢复呢?等挂起函数的工作做完了,协程就会恢复了,然后通过一些方式来告知当前线程,该执行??到???的指令了。

上图中展示的是协程在当前线程的情况。因为我们不知道挂起函数要耗时多久,所以,将3号线化成了虚线。而且,我们应该注意到一个有趣的现象,就是挂起函数将协程代码分成了两块,原本我们在代码里面写的是一个连续的逻辑,但是从协程的角度来看,它们并不连续,理解这一点非常重要。

协程的基础

上面我们画了协程的流程,协程就是一个程序的代码块,为了简单起见,下面我们称呼协程为 Program code block,简称 PCB,非常的 nice。我们做Android开发的,通常使用的是 kotlinx 封装了好多层之后的函数,里面隐藏了太多的东西,由于过于透明,反而导致难以理解,所以我们从最基础最重要的几个协程方法说起。协程的创建创建协程使用 createCoroutine 方法:

  val s = suspend {
        1
    }
    val c = s.createCoroutine(object : Continuation<Int> {
        override fun resumeWith(result: Result<Int>) {
            println("Coroutine End: $result")
        }
        override val context = EmptyCoroutineContext
    })
    c.resume(Unit)

我们先来看看 createCoroutine 的声明:

 @SinceKotlin("1.3")
@Suppress("UNCHECKED_CAST")
public fun <T> (suspend () -> T).createCoroutine(
    completion: Continuation<T>
): Continuation<Unit>

其中 suspend()->T 是 createCoroutine 函数的宿主,也就是使用了扩展函数,需要一点 kotlin 的语法知识。我们能在一个 suspend {}?这样一个东西上调用该方法就是因为这种写法了。上面的程序中,s 变量就是一个协程,可以看看源码:

@kotlin.internal.InlineOnly
@SinceKotlin("1.2")
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
public inline fun <R> suspend(noinline block: suspend () -> R): suspend () -> R = block

可以看到它就是一个代码块,我们说一个协程是一个 PCB 也不是瞎说的。协程里面会涉及到很多高阶函数的东西,这个与协程本身无关,但是会影响理解协程。继续分析协程的创建代码,我们看到有一个参数completion,它在协程执行完成后调用,实际上就是协程的完成回调。createCoroutine 返回一个 Continuation,我们像启动协程,就需要使用这个类了。

协程的启动

调用 continuation.resume(Unit) 之后,协程体会立即开始执行。这是为啥呢?其实是由于,我们的编译器在处理协程代码块的时候,生成了一个类:

里面带$符号的类,就是编译器自动生成的。当我们调用 Continuation 的方法时,它是一个 SafeContinuation 的实例,但是它只是一个包装类,真正执行逻辑的是里面的 delegate 变量,而这个 delegate 变量就是编译器生成的类的实例了。至于它是如何传递进去的,有兴趣的可以自行研究,不深入了。

反正就是 resume 方法经过一些方法调用,最后会调用到 invokeSuspend 方法里面,该方法就是储存的协程代码块的逻辑。而且最妙的地方在于,我们的 PCB,也就是 suspenc {}?被编译器改写成了 switch case 的方式。为啥要这样呢?前面说过了,挂起函数会将 PCB 分成好几个块,所以每一个块的逻辑会对应一个 case,如此这般,这般如此,协程运行时的流程就出来了。

来个例子

fun main() {
    testCreate()
}

fun testCreate() {
    val continuation = suspend {
        println("In Coroutine.")
        5
    }.createCoroutine(object : Continuation<Int> {
        override fun resumeWith(result: Result<Int>) {
            println("Coroutine End: $result")
        }
        override val context = EmptyCoroutineContext
    })

    continuation.resume(Unit)

    println("end")
}

该程序的输出如下:

In Coroutine.
Coroutine End: Success(5)
end

套一下我们上面画的图,协程里面没有挂起函数,所以协程是一个整体(该 PCB 只对应 switch 里面的一个 case),所以输出很明显和正常程序没啥区别。再看第二个例子

fun main() {
    testCreate()
}

fun testCreate() {
    val continuation = suspend {
        println("In Coroutine.")
        // 这里不同
        delay(3000)
        5
    }.createCoroutine(object : Continuation<Int> {
        override fun resumeWith(result: Result<Int>) {
            println("Coroutine End: $result")
        }
        override val context = EmptyCoroutineContext
    })

    continuation.resume(Unit)

    println("end")
}

与第一个例子不同的地方在于,我们在协程里面写了一个挂起函数 delay,delay 会切到别的线程去执行,我们后面再具体讨论。

可以想一下,现在程序的输出是什么,按照上面的模型来看,应该是:

In Coroutine.
end
Coroutine End: Success(5)

虽然思路是没错,但是由于我们是简单测试,所以 println(“end”)?执行完之后,程序就结束了,实际上是看不到 Coroutine End: Success(5)?打印出来的,实际输出如下:

In Coroutine.
end

协程的启动2

一般来讲,我们创建协程后就会立即让它开始执行,因此标准库提供了一个一步到位的API——startCoroutine。它与createCoroutine除了返回值类型不同之外,剩下的完全一致:

@SinceKotlin("1.3")
@Suppress("UNCHECKED_CAST")
public fun <T> (suspend () -> T).startCoroutine(
    completion: Continuation<T>
)

挂起函数

上面我们说到,一个挂起函数会将 PCB 挂起(切成2块),而使用 suspend 关键字我们就能创建一个挂起函数:

suspend fun nothing() {}

那么,我们使用这个函数能挂起协程吗?看代码:

fun main() {
    testSuspendFunc()
}

suspend fun nothing() {}

fun testSuspendFunc() {
    val continuation = suspend {
        println("before nothing.")
        nothing()
        println("after nothing.")
        5
    }.createCoroutine(object : Continuation<Int> {
        override fun resumeWith(result: Result<Int>) {
            println("Coroutine End: $result")
        }
        override val context = EmptyCoroutineContext
    })

    continuation.resume(Unit)

    println("end")
}

按照,我们的模型,该输出应该是:

before nothing.
end
after nothing.
Coroutine End: Success(5)

但是不是,实际输出是:

before nothing.
after nothing.
Coroutine End: Success(5)
end

为何会如此呢?很简单,应为编译器知道你只是写了一个假的挂起函数,所以并没有真正的将 PCB 分成两块。

我们看一下编译后的代码就明白了:

可以看到,编译器确实是将 PCB 进行切块了,但是它很鸡贼,它判断了挂起函数的返回值,如果这个函数的返回值不是挂起状态,说明是个冒牌货,那么就 break,然后执行后部分的代码。**我们应该注意到,这个挂起函数的返回值非常的重要。**所以,可以思考一下,我们在协程里面的任意地方添加 Thread.sleep()?函数会影响输出结果吗?假设我们能将 nothing()?的返回值改成COROUTINE_SUSPENDED又会怎样?

挂起协程

协程库里面有很多内置的挂起函数,那么它们是如何做到挂起协程的呢?

有一个基础函数可以做到,suspendCoroutine,它可以获取到它所运行在的协程对象。

@SinceKotlin("1.3")
@InlineOnly
public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T {
    contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
    return suspendCoroutineUninterceptedOrReturn { c: Continuation<T> ->
        val safe = SafeContinuation(c.intercepted())
        block(safe)
        safe.getOrThrow()
    }
}

可以看到这个函数的返回值是 safe.getOrThrow()?。

当我们启动一个协程的时候,改协程的结果状态为 UNDECIDED,然后我们在协程里面调用 suspendCoroutine,该函数会返回 COROUTINE_SUSPENDED ,导致协程逻辑上真正的被切片。

我们实践一下,看一个例子:

suspend fun suspendFunc02() = suspendCoroutine<Int> { _ -> }

fun main() {
    suspend {
        println("a")
        suspendFunc02()
        println("b")
    }.startCoroutine(object : Continuation<Unit> {
        override val context = EmptyCoroutineContext

        override fun resumeWith(result: Result<Unit>) {
            result.getOrThrow()
        }
    })
    println("c")
}

这个程序的输出是:

a
c

为何后面没有输出b呢?是因为协程一直被挂起,没有人恢复它。所以我们想写一个自己的挂起函数,还需要考虑如何恢复协程。那么如何恢复呢?suspendCoroutine 提供了一个参数,它可以操作我们的协程对象:

suspend fun suspendFunc02() = suspendCoroutine<Int> { c -> 
    c.resume(996)
}

问题在于,这样直接写行吗?我们上面贴过 suspendCoroutine 的逻辑,它会先执行 block,也就是我们写的 c.resume(996) 这行代码,然后再 ?getOrThrow 获取返回值。巧就巧在,resume 方法会改变 result 的值为 996,从而导致 getOrThrow 的返回值变成 996,也就是说协程不会挂起。所以,这样写之后,会输出:

a
b
c

做一个实验

现在我们已经掌握了协程的所有基本操作方法(创建,启动,挂起,恢复),是时候写代码了。

我们希望做这样的一个功能:

fun testSuspend() {
    val suspend = Suspend {
        for (i in 1..5) {
            g(i)
        }
    }

    println(suspend.c())
    println(suspend.c())
    println(suspend.c())
    println(suspend.c())
    println(suspend.c())
}

我们创建的 Suspend 类接受一个 block,然后这个 block 有一个 g 方法可供调用者使用。

每次调用 g 方法,Suspend 类就应该延迟生成一个数字,然后调用其 c 方法的时候,才会真正的生成并输出该数字。这个功能其实就是模拟的 python 的 generator 了。

想要做到这样,显然需要在执行 g 方法的时候,挂起当前协程,然后等待 c 方法调用的时候,再恢复协程,理解了这个,写起代码来不是很简单。

class Suspend(block: suspend Scope.() -> Unit):Scope {

    private var continuation: Continuation<Unit>? = null
    private var num:Int = 0

    init {
        val coroutineBlock: suspend Scope.() -> Unit =
            { block() }
        coroutineBlock.startCoroutine( this, object : Continuation<Unit> {
            override fun resumeWith(result: Result<Unit>) {
            }
            override val context = EmptyCoroutineContext
        })
    }

    fun c() :Int{
        val result = num
        continuation?.resume(Unit)
        return result
    }

    override suspend fun g(value:Int) : Unit {
        return suspendCoroutine { continuation ->
            this.continuation = continuation
            this.num = value
        }
    }
}

init 创建协程,g 挂起协程,c 恢复协程,没啥好说的,当然kotlin的高阶函数不在该文章的讨论范围之内。

yield函数

yield 函数是一个比较典型的例子,有助于我们理解其他的挂起函数。看 yield 的一个例子:

fun main() {
    val singleDispatcher = newSingleThreadContext("Single")

    runBlocking {
        val job = launch {
            launch {
                withContext(singleDispatcher) {
                    repeat(3) {
                        println("Task1")
                        yield()
                    }
                }
            }

            launch {
                withContext(singleDispatcher) {
                    repeat(3) {
                        println("Task2")
                        yield()
                    }
                }
            }
        }

        job.join()
    }
}

该程序的输出为:

Task1
Task2
Task1
Task2
Task1
Task2

为啥会交替执行呢?我们看看 yield 的代码:

别的先不看,我们看它的返回值是 COROUTINE_SUSPENDED,这说明它会将我们写的 repeat 函数拆分为 3 个部分:

两个子协程都运行在单线程池上,协程1在上,所以协程 1 的 repeat1 先放到线程池队列里面,然后是协程2 的 repeat 1。再看 yield 的逻辑,追踪一下发现它只是往线程池里面 post 了一个 runnable,而 runnable 会调用 resume,resume 会导致程序执行下一个切片的片段,也就是 repeat 2,如下图:

然后,由于 task 2 的 repeat1 也会 post,所以就形成了一个交替执行的效果:

这样的行为,让 yield 看起来拥有了让出协程执行权的能力,非常的牛逼。??

官方框架

Kotlin 协程的官方框架 kotlin.coroutines 是一套独立于标准库之外的以生产为目的的框架,框架本身提供了丰富的 API 来支撑生产环境中异步程序的设计和实现。也就是说,关于协程,我们虽然掌握了根本,但是这些根本操作却有N多种组合方法,创造出各种各种的使用方法。但是万变不离其宗,掌握了根本,其他的无非就是一个花心思去深入的一个过程。

就像打游戏,我的世界,萌新只会“挖三填一”,大佬能够造出摩天大厦。所以关于协程其他的东西,暂时不介绍了,希望各位能够自行学习。

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/719789.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

elk中logstash的使用

1.前言 logstash是一个相对较重的日志收集器&#xff0c;可以通过多种方式获取到日志数据&#xff0c;如tcp、日志文件、kafka、redis、rabbitmq等方式&#xff0c;还可以使用filter去过滤日志、转换日志为json格式&#xff0c;所以logstash是一个功能强大的日志收集器&#x…

MFC项目添加外部头文件和源文件后编译出现C1010错误

出现这个问题的主要原因是如果使用VC向生成工程的话&#xff0c;默认使用预编译头文件“stdafx.h”&#xff0c;这样做的目的是为了加快编译速度。 如果加入第三方c/cpp文件没有#include “stdafx.h” &#xff0c;就会报此错误。 在<解决方案管理器中>(就是可以看到工程…

NLLloss,KLDivLoss,CrossEntropyLoss三类损失函数比对

前置知识 这三个函数在深度学习模型中十分常见&#xff0c;尤其是在知识蒸馏领域&#xff0c;经常会将这三个函数进行比较 1、Softmax函数 softmax函数通常作为多分类以及归一化函数使用&#xff0c;其公式如下&#xff1a; s o f t m a x ( x ) e x i ∑ i 1 e x i soft…

Drools概述和基本原理

目录 ​编辑 一、Drools是什么&#xff1f; 二、Drools使用场景 三、Drool架构内容 3.1 总体架构 3.2 构成内容说明 3.2.1 Rules 3.2.2 Production memory 3.2.3 Facts 3.2.4 Working memory 3.2.5 Pattern matcher 3.2.6 Agenda 四、为什么要用规则引擎&#xff1f; 4.1 声明…

el-descriptions的使用

el-descriptions的使用 解释&#xff1a; 我们页面有很多无序的列表展示&#xff0c;为了高效得去开发我们得页面&#xff0c;可以借助于这个组件进行适应。图片&#xff1a; 代码&#xff1a; template部分 <el-descriptions class"margin-top" :column"…

IP数据云揭示高风险IP的来源地

在全球网络安全日临近之际&#xff0c;IP数据云揭示高风险IP的来源地。这些高风险IP以其潜在威胁和犯罪活动而闻名&#xff0c;已引起了全球范围内的关注。 根据IP数据云介绍&#xff0c;高风险IP的主要来源地是位于亚洲和东欧的国家其中包括俄罗斯、朝鲜和乌克兰等地。这些地区…

论文阅读:Segment Anything之阅读笔记

目录 引言整体结构介绍论文问答代码仓库中&#xff0c;模型哪部分转换为了ONNX格式&#xff1f;以及如何转的&#xff1f;Mask decoder部分 Transformer decoder block?如何整合image_embedding&#xff0c;image_pe, sparse_prompt_embedding和dense_prompt_embedding的&…

Android Framework基础面试必问习题~

AMS 下面是一些可能会被问到的 Android Framework 中 Activity Manager Service (AMS) 相关的面试题&#xff1a; 1.什么是 AMS&#xff1f; AMS 是 Android framework 中的一个系统进程&#xff0c;它负责管理应用程序生命周期&#xff0c;处理应用程序间的交互和协调不同组…

用Electron将Vue项目打包成桌面版软件

创建Electron项目这里是直接通过官方教程创建的 要检查 Node.js 是否正确安装&#xff0c;请在您的终端输入以下命令&#xff1a; node -vnpm -v这两个命令应输出了 Node.js 和 npm 的版本信息。 创建Electron应用程序 使用脚手架创建 Electron 应用程序遵循与其他 Node.js…

【Squid 代理服务器应用】

目录 一、Squid 代理服务器1、代理的工作机制2、代理服务器的概念及其作用3、Squid 代理的类型 二、安装 Squid 服务1&#xff0e;编译安装 Squid2&#xff0e;修改 Squid 的配置文件3&#xff0e;Squid 的运行控制1、检查配置文件语法是否正确2、启动 Squid&#xff0c;第一次…

在SpringBoot中对es集群的查询操作

在进行查询之前要先给ll索引中插入数据: POST /ll/product/1 {"id":1,"title": "小米手机Mix","category": "手机","brand": "小米","price": 2899.00,"images": "http://ima…

【数据结构】——常见排序算法(演示图+代码+算法分析)

目录 1. 常见排序算法 1.2 稳定性 2. 常见排序算法的实现 2.1 插入排序 2.1.1基本思想 2.1.2代码 2.1.4算法分析 2.2 希尔排序 2.2.1基本思想 2.2.2代码 2.2.3演示图 2.2.4算法分析 2.3 选择排序 2.3.1基本思想 2.3.2代码 2.3.3演示图 2.3.4算法分析 2.4 堆排…

[Visual Studio 报错] error 找不到指定的 SDK“Microsoft

[Visual Studio 2022 报错] error : 找不到指定的 SDK“Microsoft.NET.Sdk.Web” 问题描述&#xff1a; 在新电脑上安装了VS2022&#xff0c;打开现有项目的解决方案后的时候报了这个错&#xff0c;所有projet文件都加载失败,如图所示&#xff1a; 报错分析及解决 打开项目配…

黑客是怎样炼成的?

前言 首先我谈下对黑客&网络安全的认知&#xff0c;其实最重要的是兴趣热爱&#xff0c;不同于网络安全工程师&#xff0c;他们大都是培训机构培训出来的&#xff0c;具备的基本都是防御和白帽子技能&#xff0c;他们绝大多数的人看的是工资&#xff0c;他们是为了就业而学…

MFC扩展库BCGControlBar Pro v33.5新版亮点 - 控件、脚本管理增强

BCGControlBar库拥有500多个经过全面设计、测试和充分记录的MFC扩展类。 我们的组件可以轻松地集成到您的应用程序中&#xff0c;并为您节省数百个开发和调试时间。 BCGControlBar专业版 v33.5已正式发布了&#xff0c;此版本包含了Ribbon&#xff08;功能区&#xff09;自定义…

CSS文本样式

CSS文本样式 1、字体 友情提醒&#xff1a; 字体有没有版权&#xff1f; 省略写法 语法&#xff1a; [ [ <‘font-style’> || || <‘font-weight’> || <‘font-stretch’> ]? <‘font-size’> [ / <‘line-height’> ]? <‘font-fam…

IDEA2023.1.3自带插件禁用,减少内存占用

前言 前两个星期安装了idea2023.1.3&#xff08;之前用的一直是idea2020.3版本&#xff09;&#xff0c;我发现新版界面确实更好看一些&#xff0c;而且启动速度也非常快&#xff0c;打开多个项目也一样很快&#xff0c;都是秒开。但是吧&#xff0c;它的内存占用比idea2020.3…

【若依】框架搭建,前端向后端如何发送请求,验证码的实现,开启注册功能

若依框架 若依框架&#xff08;Ruoyi&#xff09;是一款基于Spring Boot和Spring Cloud的开源快速开发平台。它提供了一系列的基础功能和通用组件&#xff0c;能够帮助开发者快速构建企业级应用。若依框架采用了模块化的设计理念&#xff0c;用户可以选择需要的功能模块进行集…

(03)QEMU模拟ATF启动

QEMU启动 准备一个目录qemu_boot存放所有镜像文件。最终启动需要的镜像如下所示。 Image QEMU_EFI.fd bl1.bin bl2.bin bl31.bin fip.bin flash.bin rootfs.cpio.gz准备镜像 EDK2 下载QEMU_EFI。 wget http://snapshots.linaro.org/components/kernel/leg-virt-tian…

go-redis

安装redis&#xff08;docker&#xff09; sudo docker pull redis sudo docker images 在官网下载redis.conf配置文件 redis官网&#xff1a;http://www.redis.cn/download.html 将下载后的压缩包解压得到redis.conf文件&#xff0c;放到自己的目录&#xff0c;我的是/hom…