3.协程-挂起函数

2024-05-08 22:08
文章标签 函数 协程 挂起

本文主要是介绍3.协程-挂起函数,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

上一篇,我们知道了非阻塞挂起的核心是要执行一个挂起函数,挂起函数的作用就是启动线程执行耗时任务,耗时任务执行完毕,通知调用线程继续执行后续的代码。那么我们如何定义挂起函数呢?有哪些方式呢?接下来我们揭开它的面纱

一、定义挂起函数

挂起函数是协程的一个分水岭, 挂起函数前后的代码都是在调用线程执行的(当然我们可以通过调度器来改变这种状态,这个后续讲),挂起函数就是分割这2部分代码的关键。

// 1.启动一个协程
GlobalScope.launch {/*** 挂起函数前的代码*/println("1:${Thread.currentThread().name}")/*** 执行挂起函数(分水岭)*/delay(1000)/*** 挂起函数后的代码*/println("2:${Thread.currentThread().name}")
}

我们知道 delay 就是自带的一个可以做延迟的挂起函数,它是如何定义的呢?

public suspend fun delay(timeMillis: Long) {if (timeMillis <= 0) return // don't delayreturn suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)}
}

定义挂起函数的关键需要通过一个 suspend 关键词修饰函数。那么 suspend 修饰的函数是如何在做到切换线程执行耗时任务,并通知调用线程执行完毕呢?

这里提醒大家 suspend 关键词,在 Kotlin 中起到的是一个提示的作用,提示此函数是一个挂起函数,它要在协程中运行,并且内部方法要调用其他的 suspend 函数,大家先记住我这句话,因为相对于生成的 Java 字节码 suspend 存在另外的功能,现在读不懂这句话也无所谓我们继续看后面的代码。

接下来我们定义一个属于自己的延迟的挂起函数。

fun main() {GlobalScope.launch {myDelay(2000)println("延迟消息执行!")}Thread.currentThread().join()
}/*** 自定义延迟消息*/
suspend fun myDelay(timeMillis: Long) = suspendCoroutine<Unit> { continuation ->/*** 启动新的线程做延迟*/thread {/*** 线程执行延迟消息*/Thread.sleep(timeMillis)/*** 通知调用线程执行完毕,你可以继续了*/continuation.resume(Unit)}
}

看到了吗?我们创建的挂起函数是重新创建了一个线程执行延迟,然后通过一个 continuation 对象的 resume 方法,通知调用线程我执行完毕了(有没有感觉类似像写回调函数)。

看上面的 continuation 对象,有没有像回调函数啊?其实本身就是回调函数,不信你编译成 Java 代码去瞧一瞧,你就知道协程的原理了和我第一篇协程说过一样,编译器欺骗了你的眼睛,后续原理篇我带大家仔细看或者你尝试用 Java 去调用挂起函数。

continuation 可以通过 resume 可以传递一个结果,这个结果就是挂起函数的返回值。如果你不调用 continuation 的 resume 方法,那么挂起函数后的代码永远不会执行。
在这里插入图片描述
不信你就试一试。若我们要没有调用 continuation 的 resume 方法,虽然后面的代码没有被执行,但是调用线程没有被阻塞的哦,只有挂起函数内启动线程是处于阻塞状态。

二、实战

这篇章我们只讲解实战去定义挂起函数。大伙可以模仿,其实 Kotlin 为我们定义了很多 suspend 函数,方便我们去使用。
例如:当 Activity 启动后,我们就执行一个耗时操作加载数据,数据加载完毕后,用数据渲染界面。

class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {override fun onCreate(savedInstanceState: Bundle?) {.../*** 启动协程*/GlobalScope.launch(Dispatchers.Main) {/*** 展示正在加载中*/showLoadDialog()/*** 执行挂起函数,执行耗时任务*/val reslut = loadData("1")/*** 填充界面*/initView(reslut)/*** dismiss Dialog*/dismissDialog()}...}suspend fun loadData(parameter: String): List<String> {return withContext(Dispatchers.IO) {// 在线程中加载数据,我这里就模拟数据了val reslut = netRqeust(parameter)// 返回数据reslut.split("\n")}}
}

这时候有小伙伴问 withContext 是什么啊?其实 withContext 也是一个挂起函数,还记得我前面说过吗?挂起函数的作用就是执行一个其他的挂起函数。

当然我们也可以通过 suspendCoroutine 挂起函数,来定义我们的挂起函数。但是这种的形式我们还需要在自己去手动创建一个线程。

Kotlin 中为我们提供了一个 withContext 的挂起函数,它可以负责帮我们创建一个线程,并且帮我们调用 continuation 对象的 resume 方法,对应 resume 的参数就是传入的 lambda 表达式的返回值。

Dispatchers.IO 是一个线程调度器,意思是 withContext 的 lambda 表达式,要在新的线程中执行,这个后续会详细讲解。

当然如果我们不想多抽取一个 loadData 的挂起函数也是可以的,我们可以直接执行 withContext 挂起函数。

class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {override fun onCreate(savedInstanceState: Bundle?) {.../*** 启动协程*/GlobalScope.launch(Dispatchers.Main) {/*** 展示正在加载中*/showLoadDialog()// 这里:我们直接执行 withContext 挂起函数val reslut = withContext(Dispatchers.IO){// 在线程中加载数据,我这里就模拟数据了val reslut = netRqeust()// 返回数据reslut.split("\n")}/*** 填充界面*/initView(reslut)/*** dismiss Dialog*/dismissDialog()}...}
}

三、补充

这篇我们学会了如何去定义一个挂起函数,我们可以通过 withContext 来做创建线程和通知的功能,也可以通过 suspendCoroutine 挂起函数,手动处理返回结果。那我们用的时候如何选择呢?
其实很简单,如果以前存在一个方法,已经有异步请求的封装,但是你还想用协程封装下,想用协程的同步写法,那我们就应该使用 suspendCoroutine 挂起函数的形式去定义,其他的用 withContext 即可。
举个例子:
例如我们用别人的 SDK ,有一个方法是耗时的,对方已经为我们暴露了一个方法,我们只需要调用并传入 CallBack 回调即可。
此时我想将其封装成一个挂起函数要如何做呢?

fun main() {/*** sdk 为我们提供的耗时方法* 它内部肯定已经启动了线程做耗时操作*/PushManager.initPush(object :PushManager.PushCallBack{override fun onSucceed() {}override fun onError() {}})
}

若我们想封装上面的方法,便可以使用调用 suspendCoroutine 挂起函数来实现。

fun main() {GlobalScope.launch {/*** 用同步的代码,写异步的功能* 协程的核心作用*/val reslut = initPush()if (reslut=="成功"){// 执行成功的代码}}
}suspend fun initPush()=suspendCoroutine<String>{PushManager.initPush(object :PushManager.PushCallBack{override fun onSucceed() {/*** 通知成功*/it.resume("成功")}override fun onError() {/*** 通知失败*/it.resumeWithException(RuntimeException("出错"))}})
}

最后大家应该就能理清楚了吧。其实在 2.7.0 以上 Retrofit 高版本的,已经支持协程了,我们定义的请求网络接口函数,可以直接通过 suspend 修饰。
这会大家就应该思考下 Retrofit 的定义的协程的 CallAdapter 是如何通过代理去实现的挂起函数。其实就是通过调用了 suspendCoroutine 挂起函数,因为 OkHttp 内部提供异步请求的方案。
在这里插入图片描述
请看 retrofit2 包下的 KotlinExtensions 里,为 Call 定义的一个 await 扩展函数。

suspend fun <T : Any> Call<T>.await(): T {// 调用 suspendCancellableCoroutine 挂起函数return suspendCancellableCoroutine { continuation ->continuation.invokeOnCancellation {cancel()}// 注意这里,使用的就是 OkHttp enqueue 方法,执行异步请求网络enqueue(object : Callback<T> {override fun onResponse(call: Call<T>, response: Response<T>) {if (response.isSuccessful) {val body = response.body()if (body == null) {val invocation = call.request().tag(Invocation::class.java)!!val method = invocation.method()val e = KotlinNullPointerException("Response from " +method.declaringClass.name +'.' +method.name +" was null but response body type was declared as non-null")// 通知执行出错continuation.resumeWithException(e)} else {// 通知执行完成,并 resume 数据。continuation.resume(body)}} else {// 通知出错continuation.resumeWithException(HttpException(response))}}override fun onFailure(call: Call<T>, t: Throwable) {// 通知出错continuation.resumeWithException(t)}})}
}

所以说当我们调用 retrofit 生成的 suspend 请求函数的时候,已经不需要在调用一次 withContext 切换线程了,这是很多人的一个误区。

interface OneDayMsgApi {@GET("/dsapi")suspend fun getOneDayMsg(): OneDayMsgBean
}
GlobalScope.launch(Dispatchers.Main) {showDialog()/*** 很多人喜欢在用一次 withContext 切换线程* 其实这样做是多此一举的*/val oneDayMsg = withContext(Dispatchers.IO){OneDayMsgRetrofit.api.getOneDayMsg()}dissMissDialog()
}

最合适的写法是不需要写 withContext 的。

GlobalScope.launch(Dispatchers.Main) {showDialog()/*** 直接调用即可了 Retrofit 已经为我们定义了挂起函数了* 已经帮我们创建另外一个线程去执行了*/val oneDayMsg = OneDayMsgRetrofit.api.getOneDayMsg()dissMissDialog()
}

这篇关于3.协程-挂起函数的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/971569

相关文章

Python itertools中accumulate函数用法及使用运用详细讲解

《Pythonitertools中accumulate函数用法及使用运用详细讲解》:本文主要介绍Python的itertools库中的accumulate函数,该函数可以计算累积和或通过指定函数... 目录1.1前言:1.2定义:1.3衍生用法:1.3Leetcode的实际运用:总结 1.1前言:本文将详

轻松上手MYSQL之JSON函数实现高效数据查询与操作

《轻松上手MYSQL之JSON函数实现高效数据查询与操作》:本文主要介绍轻松上手MYSQL之JSON函数实现高效数据查询与操作的相关资料,MySQL提供了多个JSON函数,用于处理和查询JSON数... 目录一、jsON_EXTRACT 提取指定数据二、JSON_UNQUOTE 取消双引号三、JSON_KE

MySQL数据库函数之JSON_EXTRACT示例代码

《MySQL数据库函数之JSON_EXTRACT示例代码》:本文主要介绍MySQL数据库函数之JSON_EXTRACT的相关资料,JSON_EXTRACT()函数用于从JSON文档中提取值,支持对... 目录前言基本语法路径表达式示例示例 1: 提取简单值示例 2: 提取嵌套值示例 3: 提取数组中的值注意

Java function函数式接口的使用方法与实例

《Javafunction函数式接口的使用方法与实例》:本文主要介绍Javafunction函数式接口的使用方法与实例,函数式接口如一支未完成的诗篇,用Lambda表达式作韵脚,将代码的机械美感... 目录引言-当代码遇见诗性一、函数式接口的生物学解构1.1 函数式接口的基因密码1.2 六大核心接口的形态学

Oracle的to_date()函数详解

《Oracle的to_date()函数详解》Oracle的to_date()函数用于日期格式转换,需要注意Oracle中不区分大小写的MM和mm格式代码,应使用mi代替分钟,此外,Oracle还支持毫... 目录oracle的to_date()函数一.在使用Oracle的to_date函数来做日期转换二.日

C++11的函数包装器std::function使用示例

《C++11的函数包装器std::function使用示例》C++11引入的std::function是最常用的函数包装器,它可以存储任何可调用对象并提供统一的调用接口,以下是关于函数包装器的详细讲解... 目录一、std::function 的基本用法1. 基本语法二、如何使用 std::function

hdu1171(母函数或多重背包)

题意:把物品分成两份,使得价值最接近 可以用背包,或者是母函数来解,母函数(1 + x^v+x^2v+.....+x^num*v)(1 + x^v+x^2v+.....+x^num*v)(1 + x^v+x^2v+.....+x^num*v) 其中指数为价值,每一项的数目为(该物品数+1)个 代码如下: #include<iostream>#include<algorithm>

C++操作符重载实例(独立函数)

C++操作符重载实例,我们把坐标值CVector的加法进行重载,计算c3=c1+c2时,也就是计算x3=x1+x2,y3=y1+y2,今天我们以独立函数的方式重载操作符+(加号),以下是C++代码: c1802.cpp源代码: D:\YcjWork\CppTour>vim c1802.cpp #include <iostream>using namespace std;/*** 以独立函数

函数式编程思想

我们经常会用到各种各样的编程思想,例如面向过程、面向对象。不过笔者在该博客简单介绍一下函数式编程思想. 如果对函数式编程思想进行概括,就是f(x) = na(x) , y=uf(x)…至于其他的编程思想,可能是y=a(x)+b(x)+c(x)…,也有可能是y=f(x)=f(x)/a + f(x)/b+f(x)/c… 面向过程的指令式编程 面向过程,简单理解就是y=a(x)+b(x)+c(x)

利用matlab bar函数绘制较为复杂的柱状图,并在图中进行适当标注

示例代码和结果如下:小疑问:如何自动选择合适的坐标位置对柱状图的数值大小进行标注?😂 clear; close all;x = 1:3;aa=[28.6321521955954 26.2453660695847 21.69102348512086.93747104431360 6.25442246899816 3.342835958564245.51365061796319 4.87