当前位置: 首页 > news >正文

Kotlin笔记(七):协程

1. 协程的定义

 协程属于Kotlin中非常有特色的一项技术,因为大部分编程语言中是没有协程这个概念的。

 什么是协程呢?它其实和线程是有点类似的,可以简单地将它理解成一种轻量级的线程。要知道,线程是非常重量级的,它需要依靠操作系统的调度才能实现不同线程之间的切换。而使用协程却可以仅在编程语言的层面就能实现不同协程之间的切换,从而大大提升了并发编程的运行效率。

 举一个具体点的例子,比如我们有如下foo()和bar()两个方法:

fun foo() {a()b()c()
}
fun bar() {x()y()z()
}

 在没有开启线程的情况下,先后调用foo()和bar()这两个方法,那么理论上结果一定是a()、b()、c()执行完了以后,x()、y()、z()才能够得到执行。而如果使用了协程,在协程A中去调用foo()方法,协程B中去调用bar()方法,虽然它们仍然会运行在同一个线程当中,但是在执行foo()方法时随时都有可能被挂起转而去执行bar()方法,执行bar()方法时也随时都有可能被挂起转而继续执行foo()方法,最终的输出结果也就变得不确定了。

 可以看出,协程允许我们在单线程模式下模拟多线程编程的效果,代码执行时的挂起与恢复完全是由编程语言来控制的,和操作系统无关。这种特性使得高并发程序的运行效率得到了极大的提升,试想一下,开启10万个线程完全是不可想象的事吧?而开启10万个协程就是完全可行的。

2. 协程的基本用法

 Kotlin并没有将协程纳入标准库的API当中,而是以依赖库的形式提供的。所以如果我们想要使
用协程功能,需要先在app/build.gradle文件当中添加如下依赖库:

dependencies {...implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"//在Android项目中才会用到implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"
}

 如何开启一个协程?最简单的方式就是使用Global.launch函数,如下所示:

fun main() {GlobalScope.launch {println("codes run in coroutine scope")}
}

 GlobalScope.launch函数可以创建一个协程的作用域,这样传递给launch函数的代码块
(Lambda表达式)就是在协程中运行的了,这里我们只是在代码块中打印了一行日志。那么现在运行main()函数,日志能成功打印出来吗?如果你尝试一下,会发现没有任何日志输出。

 这是因为,Global.launch函数每次创建的都是一个顶层协程,这种协程当应用程序运行结束时也会跟着一起结束。刚才的日志之所以无法打印出来,就是因为代码块中的代码还没来得及运行,应用程序就结束了。

 要解决这个问题也很简单,我们让程序延迟一段时间再结束就行了,如下所示:

fun main() {GlobalScope.launch {println("codes run in coroutine scope")}Thread.sleep(1000)
}

 这里使用Thread.sleep()方法让主线程阻塞1秒钟,现在重新运行程序,你会发现日志可以正常打印出来了.

 可是这种写法还是存在问题,如果代码块中的代码在1秒钟之内不能运行结束,那么就会被强制中断。观察如下代码:

fun main() {GlobalScope.launch {println("codes run in coroutine scope")delay(1500)println("codes run in coroutine scope finished")}Thread.sleep(1000)
}

 我们在代码块中加入了一个delay()函数,并在之后又打印了一行日志。delay()函数可以让当前协程延迟指定时间后再运行,但它和Thread.sleep()方法不同。delay()函数是一个非阻塞式的挂起函数,它只会挂起当前协程,并不会影响其他协程的运行。而Thread.sleep()方法会阻塞当前的线程,这样运行在该线程下的所有协程都会被阻塞。注意,delay()函数只能在协程的作用域或其他挂起函数中调用。

 这里我们让协程挂起1.5秒,但是主线程却只阻塞了1秒,最终会是什么结果呢?重新运行程序,你会发现代码块中新增的一条日志并没有打印出来,因为它还没能来得及运行,应用程序就已经结束了.

 那么有没有什么办法能让应用程序在协程中所有代码都运行完了之后再结束呢?当然也是有的,借助runBlocking函数就可以实现这个功能:

fun main() {runBlocking {println("codes run in coroutine scope")delay(1500)println("codes run in coroutine scope finished")}
}

 runBlocking函数同样会创建一个协程的作用域,但是它可以保证在协程作用域内的所有代码和子协程没有全部执行完之前一直阻塞当前线程。需要注意的是,runBlocking函数通常只应该在测试环境下使用,在正式环境中使用容易产生一些性能上的问题。

 虽说现在我们已经能够让代码在协程中运行了,可是好像并没有体会到什么特别的好处。这是
因为目前所有的代码都是运行在同一个协程当中的,而一旦涉及高并发的应用场景,协程相比于线程的优势就能体现出来了。

 那么如何才能创建多个协程呢?很简单,使用launch函数就可以了,如下所示:

fun main() {runBlocking {launch {println("launch1")delay(1000)println("launch1 finished")}launch {println("launch2")delay(1000)println("launch2 finished")}}
}

 注意这里的launch函数和我们刚才所使用的GlobalScope.launch函数不同。首先它必须在协程的作用域中才能调用,其次它会在当前协程的作用域下创建子协程。子协程的特点是如果外层作用域的协程结束了,该作用域下的所有子协程也会一同结束。相比而言,GlobalScope.launch函数创建的永远是顶层协程,这一点和线程比较像,因为线程也没有层级这一说,永远都是顶层的。

 这两个子协程实际却运行在同一个线程当中,只是由编程语言来决定如何在多个协程之间进行调度,让谁运行,让谁挂起。调度的过程完全不需要操作系统参与,这也就使得协程的并发效率会出奇得高。

 那么具体会有多高呢?我们来做下实验就知道了,代码如下所示:

fun main() {val start = System.currentTimeMillis()runBlocking {repeat(100000) {launch {println(".")}}}val end = System.currentTimeMillis()println(end - start)
}

 这里使用repeat函数循环创建了10万个协程,不过在协程当中并没有进行什么有意义的操作,
只是象征性地打印了一个点,然后记录一下整个操作的运行耗时。

 这里仅仅耗时了961毫秒,这足以证明协程有多么高效。试想一下,如果开启的是
10万个线程,程序或许已经出现OOM异常了。

 随着launch函数中的逻辑越来越复杂,可能你需要将部分代码提取到一个单独的函数中。这个时候就产生了一个问题:我们在launch函数中编写的代码是拥有协程作用域的,但是提取到一个单独的函数中就没有协程作用域了,那么我们该如何调用像delay()这样的挂起函数呢?

 为此Kotlin提供了一个suspend关键字,使用它可以将任意函数声明成挂起函数,而挂起函数之间都是可以互相调用的,如下所示:

suspend fun printDot() {println(".")delay(1000)
}

 但是,suspend关键字只能将一个函数声明成挂起函数,是无法给它提供协程作用域的。比如你现在尝试在printDot()函数中调用launch函数,一定是无法调用成功的,因为launch函数要求必须在协程作用域当中才能调用。

 这个问题可以借助coroutineScope函数来解决。coroutineScope函数也是一个挂起函数,因此可以在任何其他挂起函数中调用。它的特点是会继承外部的协程的作用域并创建一个子协程,借助这个特性,我们就可以给任意挂起函数提供协程作用域了。示例写法如下:

suspend fun printDot() = coroutineScope {launch {println(".")delay(1000)}
}

 另外,coroutineScope函数和runBlocking函数还有点类似,它可以保证其作用域内的所有代码和子协程在全部执行完之前,外部的协程会一直被挂起。

 coroutineScope函数和runBlocking函数的作用是有点类似的,但是coroutineScope函数只会阻塞当前协程,既不影响其他协程,也不影响任何线程,因此是不会造成任何性能上的问题的。而runBlocking函数由于会挂起外部线程,如果你恰好又在主线程中当中调用它的话,那么就有可能会导致界面卡死的情况,所以不太推荐在实际项目中使用。

3. 更多作用域构建器

 GlobalScope.launch、runBlocking、launch、coroutineScope这几种作用域构建器,它们都可以用于创建一个新的协程作用域。不过GlobalScope.launch和runBlocking函数是可以在任意地方调用的,coroutineScope函数可以在协程作用域或挂起函数中调用,而launch函数只能在协程作用域中调用。

 前面已经说了,runBlocking由于会阻塞线程,因此只建议在测试环境下使用。而GlobalScope.launch由于每次创建的都是顶层协程,一般也不太建议使用,除非你非常明确就是要创建顶层协程。

 为什么说不太建议使用顶层协程呢?主要还是因为它管理起来成本太高了。举个例子,比如我们在某个Activity中使用协程发起了一条网络请求,由于网络请求是耗时的,用户在服务器还没来得及响应的情况下就关闭了当前Activity,此时按理说应该取消这条网络请求,或者至少不应该进行回调,因为Activity已经不存在了,回调了也没有意义。

 那么协程要怎样取消呢?不管是GlobalScope.launch函数还是launch函数,它们都会返回一个Job对象,只需要调用Job对象的cancel()方法就可以取消协程了,如下所示:

val job = GlobalScope.launch {// 处理具体的逻辑
}
job.cancel()

 但是如果我们每次创建的都是顶层协程,那么当Activity关闭时,就需要逐个调用所有已创建协程的cancel()方法,试想一下,这样的代码是不是根本无法维护?

 因此,GlobalScope.launch这种协程作用域构建器,在实际项目中也是不太常用的。下面演示一下实际项目中比较常用的写法:

val job = Job()
val scope = CoroutineScope(job)
scope.launch {// 处理具体的逻辑
}
job.cancel()

 可以看到,我们先创建了一个Job对象,然后把它传入CoroutineScope()函数当中,注意这里的CoroutineScope()是个函数,虽然它的命名更像是一个类。CoroutineScope()函数会返回一个CoroutineScope对象,这种语法结构的设计更像是我们创建了一个CoroutineScope的实例,可能也是Kotlin有意为之的。有了CoroutineScope对象之后,就可以随时调用它的launch函数来创建一个协程了。

 现在所有调用CoroutineScope的launch函数所创建的协程,都会被关联在Job对象的作用域
下面。这样只需要调用一次cancel()方法,就可以将同一作用域内的所有协程全部取消,从而大大降低了协程管理的成本。

 调用launch函数可以创建一个新的协程,但是launch函数只能用于执行一段逻辑,却不能获取执行的结果,因为它的返回值永远是一个Job对象。那么有没有什么办法能够创建一个协程并获取它的执行结果呢?当然有,使用async函数就可以实现。

 async函数必须在协程作用域当中才能调用,它会创建一个新的子协程并返回一个Deferred对象,如果我们想要获取async函数代码块的执行结果,只需要调用Deferred对象的await()方法即可,代码如下所示:

fun main() {runBlocking {val result = async {5 + 5}.await()println(result)}
}

 不过async函数的奥秘还不止于此。事实上,在调用了async函数之后,代码块中的代码就会立刻开始执行。当调用await()方法时,如果代码块中的代码还没执行完,那么await()方法会将当前协程阻塞住,直到可以获得async函数的执行结果。

fun main() {runBlocking {val start = System.currentTimeMillis()val result1 = async {delay(1000)5 + 5}.await()val result2 = async {delay(1000)4 + 6}.await()println("result is ${result1 + result2}.")val end = System.currentTimeMillis()println("cost ${end - start} ms.")}
}

 这种写法明显是非常低效的,因为两个async函数完全可以同时执行从而提高运行效率。现在对上述代码使用如下的写法进行修改:

fun main() {runBlocking {val start = System.currentTimeMillis()val deferred1 = async {delay(1000)5 + 5}val deferred2 = async {delay(1000)4 + 6}println("result is ${deferred1.await() + deferred2.await()}.")val end = System.currentTimeMillis()println("cost ${end - start} milliseconds.")}
}

 现在我们不在每次调用async函数之后就立刻使用await()方法获取结果了,而是仅在需要用到async函数的执行结果时才调用await()方法进行获取,这样两个async函数就变成一种并行关系了。

 withContext()函数是一个挂起函数,大体可以将它理解成async函数的一种简化版写法,示例写法如下:

fun main() {runBlocking {val result = withContext(Dispatchers.Default) {5 + 5}println(result)}
}

 调用withContext()函数之后,会立即执行代码块中的代码,同时将外部协程挂起。当代码块中的代码全部执行完之后,会将最后一行的执行结果作为withContext()函数的返回值返回,因此基本上相当于val result = async{ 5 + 5}.await()的写法。唯一不同的是,withContext()函数强制要求我们指定一个线程参数, 关于这个参数我准备好好讲一讲。

 协程是一种轻量级的线程的概念,因此很多传统编程情况下需要开启多线程执行的并发任务,现在只需要在一个线程下开启多个协程来执行就可以了。但是这并不意味着我们就永远不需要开启线程了,比如说Android中要求网络请求必须在子线程中进行,即使你开启了协程去执行网络请求,假如它是主线程当中的协程,那么程序仍然会出错。这个时候我们就应该通过线程参数给协程指定一个具体的运行线程。

 线程参数主要有以下3种值可选:Dispatchers.Default、Dispatchers.IO和Dispatchers.Main。Dispatchers.Default表示会使用一种默认低并发的线程策略,当你要执行的代码属于计算密集型任务时,开启过高的并发反而可能会影响任务的运行效率,此时就可以使用Dispatchers.Default。Dispatchers.IO表示会使用一种较高并发的线程策略,当你要执行的代码大多数时间是在阻塞和等待中,比如说执行网络请求时,为了能够支持更高的并发数量,此时就可以使用Dispatchers.IO。Dispatchers.Main则表示不会开启子线程,而是在Android主线程中执行代码,但是这个值只能在Android项目中使用,纯Kotlin程序使用这种类型的线程参数会出现错误。

 事实上,在我们刚才所学的协程作用域构建器中,除了coroutineScope函数之外,其他所有的函数都是可以指定这样一个线程参数的,只不过withContext()函数是强制要求指定的,而其他函数则是可选的。

 到目前为止,你已经掌握了协程中最常用的一些用法,并且了解了协程的主要用途就是可以大幅度地提升并发编程的运行效率。但实际上,Kotlin中的协程还可以对传统回调的写法进行优化,从而让代码变得更加简洁,那么接下来我们就开始学习这部分的内容。

4. 使用协程简化回调的写法

 回调机制基本上是依靠匿名类来实现的,但是匿名类的写法通常比较烦琐,比如如下代码:

HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {override fun onFinish(response: String) {// 得到服务器返回的具体内容}override fun onError(e: Exception) {// 在这里对异常情况进行处理}
})

 在多少个地方发起网络请求,就需要编写多少次这样的匿名类实现。这不禁引起了我们的思考,有没有更加简单一点的写法呢?

 在过去,可能确实没有什么更加简单的写法了。不过现在,Kotlin的协程使我们的这种设想成为了可能,只需要借助suspendCoroutine函数就能将传统回调机制的写法大幅简化,下面我们就来具体学习一下。

 suspendCoroutine函数必须在协程作用域或挂起函数中才能调用,它接收一个Lambda表达式参数,主要作用是将当前协程立即挂起,然后在一个普通的线程中执行Lambda表达式中的代码。Lambda表达式的参数列表上会传入一个Continuation参数,调用它的resume()方法或resumeWithException()可以让协程恢复执行。

 了解了suspendCoroutine函数的作用之后,接下来我们就可以借助这个函数来对传统的回调写法进行优化。首先定义一个request()函数,代码如下所示:

suspend fun request(address: String): String {return suspendCoroutine { continuation ->HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {override fun onFinish(response: String) {continuation.resume(response)}override fun onError(e: Exception) {continuation.resumeWithException(e)}})}
}

 可以看到,request()函数是一个挂起函数,并且接收一个address参数。在request()函数的内部,我们调用了刚刚介绍的suspendCoroutine函数,这样当前协程就会被立刻挂起,而Lambda表达式中的代码则会在普通线程中执行。接着我们在Lambda表达式中调用HttpUtil.sendHttpRequest()方法发起网络请求,并通过传统回调的方式监听请求结果。如果请求成功就调用Continuation的resume()方法恢复被挂起的协程,并传入服务器响应的数据,该值会成为suspendCoroutine函数的返回值。如果请求失败,就调用Continuation的resumeWithException()恢复被挂起的协程,并传入具体的异常原因。

 这里不是仍然使用了传统回调的写法吗?代码怎么就变得更加简化了?这是因为,不管之后我们要发起多少次网络请求,都不需要再重复进行回调实现了。比如说获取百度首页的响应数据,就可以这样写

suspend fun getBaiduResponse() {try {val response = request("https://www.baidu.com/")// 对服务器响应的数据进行处理} catch (e: Exception) {// 对异常情况进行处理}
}

 有没有觉得代码变得清爽了很多呢?由于 getBaiduResponse()是一个挂起函数,因此当它调用了request()函数时,当前的协程就会被立刻挂起,然后一直等待网络请求成功或失败后,当前协程才能恢复运行。这样即使不使用回调的写法,我们也能够获得异步网络请求的响应数据,而如果请求失败,则会直接进入catch语句当中。

 不过这里你可能又会产生新的疑惑,getBaiduResponse()函数被声明成了挂起函数,这样它也只能在协程作用域或其他挂起函数中调用了,使用起来是不是非常有局限性?确实如此,因为suspendCoroutine函数本身就是要结合协程一起使用的。不过通过合理的项目架构设计,我们可以轻松地将各种协程的代码应用到一个普通的项目当中.

 事实上,suspendCoroutine函数几乎可以用于简化任何回调的写法,比如之前使用Retrofit来发起网络请求需要这样写:

val appService = ServiceCreator.create<AppService>()
appService.getAppData().enqueue(object : Callback<List<App>> {override fun onResponse(call: Call<List<App>>, response: Response<List<App>>) {// 得到服务器返回的数据}override fun onFailure(call: Call<List<App>>, t: Throwable) {// 在这里对异常情况进行处理}
})

 不用担心,使用suspendCoroutine函数,我们马上就能对上述写法进行大幅度的简化。由于不同的Service接口返回的数据类型也不同,所以这次我们不能像刚才那样针对具体的类型进行编程了,而是要使用泛型的方式。定义一个await()函数,代码如下所示:

suspend fun <T> Call<T>.await(): T {return suspendCoroutine { continuation ->enqueue(object : Callback<T> {override fun onResponse(call: Call<T>, response: Response<T>) {val body = response.body()if (body != null) continuation.resume(body)else continuation.resumeWithException(RuntimeException("response body is null"))}override fun onFailure(call: Call<T>, t: Throwable) {continuation.resumeWithException(t)}})}
}

 这段代码相比于刚才的request()函数又复杂了一点。首先await()函数仍然是一个挂起函数,然后我们给它声明了一个泛型T,并将await()函数定义成了Call的扩展函数,这样所有返回值是Call类型的Retrofit网络请求接口就都可以直接调用await()函数了。

 接着,await()函数中使用了suspendCoroutine函数来挂起当前协程,并且由于扩展函数的原因,我们现在拥有了Call对象的上下文,那么这里就可以直接调用enqueue()方法让Retrofit发起网络请求。接下来,使用同样的方式对Retrofit响应的数据或者网络请求失败的情况进行处理就可以了。另外还有一点需要注意,在onResponse()回调当中,我们调用body()方法解析出来的对象是可能为空的。如果为空的话,这里的做法是手动抛出一个异常,你也可以根据自己的逻辑进行更加合适的处理。

 有了await()函数之后,我们调用所有Retrofit的Service接口都会变得极其简单,比如刚才同样的功能就可以使用如下写法进行实现:

suspend fun getAppData() {try {val appList = ServiceCreator.create<AppService>().getAppData().await()// 对服务器响应的数据进行处理} catch (e: Exception) {// 对异常情况进行处理}
}

相关文章:

Kotlin笔记(七):协程

1. 协程的定义 协程属于Kotlin中非常有特色的一项技术&#xff0c;因为大部分编程语言中是没有协程这个概念的。 什么是协程呢&#xff1f;它其实和线程是有点类似的&#xff0c;可以简单地将它理解成一种轻量级的线程。要知道&#xff0c;线程是非常重量级的&#xff0c;它需要…...

存储优化知识复习二详细版解析

存储优化 知识复习二 一、 选择题 1、 对数据库调优的方法中&#xff0c;最困难但是最有成效的是( )。 A、优化表的架构设计 B、添加内存 C、索引优化 D、查询语句优化 【参考答案】A2、 防止与处理死锁的方法有&#xff08; &#xff09;。 A、尽量避免或尽快处理阻塞 B、访…...

《持续交付:发布可靠软件的系统方法》- 读书笔记(七)

持续交付&#xff1a;发布可靠软件的系统方法&#xff08;七&#xff09; 第 7 章 提交阶段7.1 引言7.2 提交阶段的原则和实践7.2.1 提供快速有用的反馈7.2.2 何时令提交阶段失败7.2.3 精心对待提交阶段7.2.4 让开发人员也拥有所有权7.2.5 在超大项目团队中指定一个构建负责人 …...

Go源码实现使用多线程并发下载大文件的功能

摘要&#xff1a;Go语言编码实现了使用多线程并发下载文件的功能。 1. 代码流程介绍 1. 获取系统的CPU核心数量&#xff0c;并将其作为线程数的参考值&#xff0c;并打印出来。 2. 定义要下载的文件的URL、线程数和输出文件名。 3. 使用getFileSize()函数获取文件大小&#xf…...

Python基础入门例程1-NP1 Hello World!

描述 将字符串 Hello World! 存储到变量str中&#xff0c;再使用print语句将其打印出来。 输入描述&#xff1a; 无 输出描述&#xff1a; 一行输出字符串Hello World! 解答&#xff1a; str "Hello World!" print(str) 解释说明&#xff1a; 赋值变量&…...

前端面试题10.23

解决的最复杂的前端问题&#xff0c;介绍一下 最复杂的前端问题之一是浏览器兼容性&#xff0c;不同浏览器对网页的渲染方式存在差异&#xff0c;需要针对不同浏览器做兼容性处理。此外&#xff0c;前端性能优化也是一个复杂的问题&#xff0c;需要综合考虑网页加载速度、渲染…...

DYC算法开发与测试(基于ModelBase实现)

ModelBase是经纬恒润开发的车辆仿真软件&#xff0c;包含两个大版本&#xff1a;动力学版本、智能驾驶版本。动力学版包含高精度动力学模型&#xff0c;能很好地复现车辆在实际道路中运行的各种状态变化&#xff0c;可用于乘用车、商用车动力底盘系统算法开发、控制器仿真测试&…...

第四章 路由基础

目录 4.1 路由器概述 4.1.1 路由器定义 4.1.2 路由器工作原理 4.1.3 路由表的生成方式 &#xff08;1&#xff09;直连路由 &#xff08;2&#xff09;静态路由 &#xff08;3&#xff09;动态路由 4.1.4 路由器的接口 &#xff08;1&#xff09;配置接口 &#xff0…...

Java逻辑运算符(、||和!),Java关系运算符

逻辑运算符把各个运算的关系表达式连接起来组成一个复杂的逻辑表达式&#xff0c;以判断程序中的表达式是否成立&#xff0c;判断的结果是 true 或 false。 逻辑运算符是对布尔型变量进行运算&#xff0c;其结果也是布尔型&#xff0c;具体如表 1 所示。 表 1 逻辑运算符的用…...

三个设备文件

...

Java赋值运算符(=)

赋值运算符是指为变量或常量指定数值的符号。赋值运算符的符号为“”&#xff0c;它是双目运算符&#xff0c;左边的操作数必须是变量&#xff0c;不能是常量或表达式。 其语法格式如下所示&#xff1a; 变量名称表达式内容 在 Java 语言中&#xff0c;“变量名称”和“表达式…...

提升药店效率:山海鲸医药零售大屏的成功案例

在医药行业中&#xff0c;特别是医药零售领域&#xff0c;高效的药品管理和客户服务至关重要。随着科技的飞速发展&#xff0c;数字化解决方案已经成为提高医药零售管控效率的有效工具之一。其中&#xff0c;医药零售管控大屏作为一种强大的工具&#xff0c;正在以独特的方式改…...

使用Fragement(碎片)

一、Fragment简介 屏幕大小的差距可能会使同样的界面在不同设备上显示出不同的效果&#xff0c;为了能同时兼顾到手机和平板电脑的开发&#xff0c;从Android3.0版本开始提供了Fragment。 Fragment&#xff08;碎片&#xff09;是一种嵌入在Activity中的UI片段&#xff0c;它…...

种花问题(Python题目)

假设有一个很长的花坛&#xff0c;一部分地块种植了花&#xff0c;另一部分却没有。可是&#xff0c;花不能种植在相邻的地块上&#xff0c;它们会争夺水源&#xff0c;两者都会死去。 给你一个整数数组 flowerbed 表示花坛&#xff0c;由若干 0 和 1 组成&#xff0c;其中 0 …...

STM32入门F4

学习资料&#xff1a;杨桃电子&#xff0c;官网&#xff1a;洋桃电子 | 杜洋工作室 www.doyoung.net 嵌入式开发核心宗旨&#xff1a;以最适合的性能、功能、成本来完成最有性价比的产品开发。 1.为什么要学F407 STM32F103系列与STM32F407系列对照表&#xff1a; 2.F4系列命…...

【30】c++设计模式——>状态模式

状态模式概述 状态模式是一种行为型设计模式&#xff0c;它可以让一个对象在其内部状态发生变化时更改其行为。通过将每个状态封装成一个独立的类&#xff0c;我们可以使状态之间互相独立&#xff0c;并且使得状态的切换变得更加灵活、可扩展。&#xff08;多个状态之间可以相…...

中文编程开发语言编程实际案例:程序控制灯电路以及桌球台球室用这个程序计时计费

中文编程开发语言编程实际案例&#xff1a;程序控制灯电路以及桌球台球室用这个程序计时计费 上图为&#xff1a;程序控制的硬件设备电路图 上图为&#xff1a;程序控制灯的开关软件截图&#xff0c;适用范围比如&#xff1a;台球厅桌球室的计时计费管理&#xff0c;计时的时候…...

【python】高斯日记

题目&#xff1a; """ 题目描述&#xff1a; 高斯出生于1777年4月30日&#xff0c;记作1777-4-30。在此基础上&#xff0c;我们希望你写一个程序&#xff0c;给定一个数字n&#xff0c;表示从高斯出生的那天算起的第n天&#xff0c;输出这一天的具体日期。例如&…...

[论文笔记]MobileBERT

引言 今天带来一篇关于量化的论文MobileBERT,题目翻译过来是:一种适用于资源有限设备的紧凑型任务无关BERT模型。模型的简称是MobileBERT,意思是作者的这个BERT模型可以部署到手机端。 本篇工作,作者提出了MobileBERT用于压缩和加速BERT模型。与原始BERT一样,MobileBERT…...

【Spring Cloud】如何确定微服务项目的Spring Boot、Spring Cloud、Spring Cloud Alibaba的版本

文章目录 1. 版本选择2. 用脚手架快速生成微服务的pom.xml3. 创建一个父工程4. 代码地址 本文描述如何确定微服务项目的Spring Boot、Spring Cloud、Spring Cloud Alibaba的版本。 1. 版本选择 我们知道Spring Boot、Spring Cloud、Spring Cloud Alibaba的版本选择一致性非常重…...

【RockeMQ】第2节|RocketMQ快速实战以及核⼼概念详解(二)

升级Dledger高可用集群 一、主从架构的不足与Dledger的定位 主从架构缺陷 数据备份依赖Slave节点&#xff0c;但无自动故障转移能力&#xff0c;Master宕机后需人工切换&#xff0c;期间消息可能无法读取。Slave仅存储数据&#xff0c;无法主动升级为Master响应请求&#xff…...

Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信

文章目录 Linux C语言网络编程详细入门教程&#xff1a;如何一步步实现TCP服务端与客户端通信前言一、网络通信基础概念二、服务端与客户端的完整流程图解三、每一步的详细讲解和代码示例1. 创建Socket&#xff08;服务端和客户端都要&#xff09;2. 绑定本地地址和端口&#x…...

技术栈RabbitMq的介绍和使用

目录 1. 什么是消息队列&#xff1f;2. 消息队列的优点3. RabbitMQ 消息队列概述4. RabbitMQ 安装5. Exchange 四种类型5.1 direct 精准匹配5.2 fanout 广播5.3 topic 正则匹配 6. RabbitMQ 队列模式6.1 简单队列模式6.2 工作队列模式6.3 发布/订阅模式6.4 路由模式6.5 主题模式…...

怎么让Comfyui导出的图像不包含工作流信息,

为了数据安全&#xff0c;让Comfyui导出的图像不包含工作流信息&#xff0c;导出的图像就不会拖到comfyui中加载出来工作流。 ComfyUI的目录下node.py 直接移除 pnginfo&#xff08;推荐&#xff09;​​ 在 save_images 方法中&#xff0c;​​删除或注释掉所有与 metadata …...

从面试角度回答Android中ContentProvider启动原理

Android中ContentProvider原理的面试角度解析&#xff0c;分为​​已启动​​和​​未启动​​两种场景&#xff1a; 一、ContentProvider已启动的情况 1. ​​核心流程​​ ​​触发条件​​&#xff1a;当其他组件&#xff08;如Activity、Service&#xff09;通过ContentR…...

LangFlow技术架构分析

&#x1f527; LangFlow 的可视化技术栈 前端节点编辑器 底层框架&#xff1a;基于 &#xff08;一个现代化的 React 节点绘图库&#xff09; 功能&#xff1a; 拖拽式构建 LangGraph 状态机 实时连线定义节点依赖关系 可视化调试循环和分支逻辑 与 LangGraph 的深…...

MyBatis中关于缓存的理解

MyBatis缓存 MyBatis系统当中默认定义两级缓存&#xff1a;一级缓存、二级缓存 默认情况下&#xff0c;只有一级缓存开启&#xff08;sqlSession级别的缓存&#xff09;二级缓存需要手动开启配置&#xff0c;需要局域namespace级别的缓存 一级缓存&#xff08;本地缓存&#…...

uniapp 集成腾讯云 IM 富媒体消息(地理位置/文件)

UniApp 集成腾讯云 IM 富媒体消息全攻略&#xff08;地理位置/文件&#xff09; 一、功能实现原理 腾讯云 IM 通过 消息扩展机制 支持富媒体类型&#xff0c;核心实现方式&#xff1a; 标准消息类型&#xff1a;直接使用 SDK 内置类型&#xff08;文件、图片等&#xff09;自…...

【Veristand】Veristand环境安装教程-Linux RT / Windows

首先声明&#xff0c;此教程是针对Simulink编译模型并导入Veristand中编写的&#xff0c;同时需要注意的是老用户编译可能用的是Veristand Model Framework&#xff0c;那个是历史版本&#xff0c;且NI不会再维护&#xff0c;新版本编译支持为VeriStand Model Generation Suppo…...

Python常用模块:time、os、shutil与flask初探

一、Flask初探 & PyCharm终端配置 目的: 快速搭建小型Web服务器以提供数据。 工具: 第三方Web框架 Flask (需 pip install flask 安装)。 安装 Flask: 建议: 使用 PyCharm 内置的 Terminal (模拟命令行) 进行安装,避免频繁切换。 PyCharm Terminal 配置建议: 打开 Py…...