Kotlin 协程基础三 —— 结构化并发(二)
Kotlin 协程基础系列:
Kotlin 协程基础一 —— 总体知识概述
Kotlin 协程基础二 —— 结构化并发(一)
Kotlin 协程基础三 —— 结构化并发(二)
Kotlin 协程基础四 —— CoroutineScope 与 CoroutineContext
Kotlin 协程基础五 —— Channel
Kotlin 协程基础六 —— Flow
Kotlin 协程基础七 —— Flow 操作符(一)
Kotlin 协程基础八 —— Flow 操作符(二)
Kotlin 协程基础九 —— SharedFlow 与 StateFlow
Kotlin 协程基础十 —— 协作、互斥锁与共享变量
本篇将继续结构化并发的话题,来介绍结构化异常管理的相关内容。
1、协程的结构化异常管理
如果一个协程抛异常,它所在的整个协程树上的其他协程(向上到根协程,向下到子孙协程)都会被取消,因此协程发生异常的后果是十分严重的。
1.1 协程的取消与异常
本质上,协程异常与取消采用的是同一套处理逻辑。只不过取消协程抛出的是一个特殊异常 CancellationException,并进行特殊处理以取消协程;而协程异常是抛出除 CancellationException 以外其他的异常。
先查看抛出异常前后父子协程的状态:
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)var childJob: Job? = nullval parentJob = scope.launch {childJob = launch {println("Child started")delay(3000)println("Child finished")}delay(1000)throw IllegalStateException("Wrong User!")}// 抛异常之前打印两个协程的状态delay(500)println("isActive: parent - ${parentJob.isActive}, child - ${childJob?.isActive}")println("isCancelled: parent - ${parentJob.isCancelled}, child - ${childJob?.isCancelled}")// 抛异常之后打印两个协程的状态delay(1500)println("isActive: parent - ${parentJob.isActive}, child - ${childJob?.isActive}")println("isCancelled: parent - ${parentJob.isCancelled}, child - ${childJob?.isCancelled}")delay(10000)
}
运行结果:
Child started
isActive: parent - true, child - true
isCancelled: parent - false, child - false
Exception in thread "DefaultDispatcher-worker-1" java.lang.RuntimeException: Exception while trying to handle coroutine exceptionat kotlinx.coroutines.CoroutineExceptionHandlerKt.handlerException(CoroutineExceptionHandler.kt:33)at
...
isActive: parent - false, child - false
isCancelled: parent - true, child - true
显而易见,在父协程抛出异常后,父协程取消连带子协程一起被取消了。
注意,应用程序发生未被捕获的异常导致程序崩溃是 Android 的规则,而不是普通 JVM 的规则。JVM 中发生异常只会导致线程崩溃,而不会导致整个应用崩溃。因此上面的运行结果在崩溃后输出的父子协程状态数据也是正确的、可参考的。
假如让父协程抛出 CancellationException 代替 IllegalStateException,你会发现运行结果除了没有异常信息外,其余结果是一样的:
Child started
isActive: parent - true, child - true
isCancelled: parent - false, child - false
isActive: parent - false, child - false
isCancelled: parent - true, child - trueProcess finished with exit code 0
这是因为,协程的取消与异常处理,在底层走的是同一套流程,只不过取消处理相对于较为完整的异常处理,过程有所简化。
1.2 取消与异常的不同点
虽然协程的取消与异常走的是同一套逻辑,但它们又有两点显著的不同:
- 取消的作用方向是单向的,只会取消自己与所有的下层协程;而异常的作用方向是双向的,除了取消自己,也会向上取消所有上层协程,向下取消所有下层协程,即取消整个协程树
- 取消有两种方式,可以在协程内部抛 CancellationException,也可以在协程外部调用 cancel();而异常流程的触发只有一种方式,就是在协程内抛异常
我们结合源码来解释以上两点。
当取消子协程时,取消流程会调用到 JobSupport 的 childCancelled():
// JobSupport 实现了 Job 接口,作为所有 Job 的父类被继承public open fun childCancelled(cause: Throwable): Boolean {if (cause is CancellationException) return truereturn cancelImpl(cause) && handlesException}
如果取消的原因是 CancellationException 就返回 true 不继续执行后续的 cancelImpl(),该函数内部正是处理取消父协程的代码。
为什么会有这种区别?这实际上体现了协程的设计思想。
两个协程之所以被写成父子协程的关系,是因为二者在逻辑上有相互包含的关系(当然,从运行角度,二者是并行关系),子协程执行的内容通常是父协程任务的子流程。
从正常的逻辑上讲,当一个大流程被取消时,它内部的子流程也就没用了。因此,才会给协程设计这种取消时连带的性质。类似的,父协程等待所有子协程完成后再结束,也是因为所有子流程完成后整个大流程才算完成,所以才有父协程等待子协程的性质。
子协程取消,对外部大流程而言可能只是一个正常的事件而已,因此子协程的取消不会导致父协程的取消。
但如果子协程抛异常了,通常意味着父协程被损坏,影响整个大流程。因此子协程抛异常,会导致父协程也以该异常为原因而取消。
当然,协程源码实际上也存在通过 cancel() 触发异常流程的函数:
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Since 1.2.0, binary compatibility with versions <= 1.1.x")public fun cancel(cause: Throwable? = null): Boolean
但是明显能看到该函数是 HIDDEN 的,不提供给开发者使用。实际上也很好理解,取消函数就触发取消流程,不要触发异常流程搞得逻辑混乱。
关于异常还需注意一点,用于取消的 CancellationException 除了用来处理取消协程之外就别无用处;而异常流程里抛出的异常对象除了用来取消协程,还会把该异常对象暴露给线程。直接体现就是本节举例的,协程抛出 IllegalStateException 导致线程崩溃输出 log 信息。
2、CoroutineExceptionHandler
先从一个例子看起:
// 示例代码 1
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)scope.launch {try {throw RuntimeException()} catch (e: Exception) {}}delay(10000)
}
在协程内部将有可能抛出异常的代码用 try-catch 包起来,可以捕获异常。但倘若将 try-catch 挪到协程 launch 之外:
// 示例代码 2
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)try {scope.launch {throw RuntimeException()}} catch (e: Exception) {}delay(10000)
}
这个异常就无法被捕获。这种形式类似于:
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)try {thread {throw RuntimeException()}} catch (e: Exception) {}delay(10000)
}
子线程内抛出的异常,在主线程去捕获,肯定是捕获不到的。对于协程也是一样的,像示例代码 2 那样,try-catch 只能捕获到协程启动阶段可能抛出的异常,也就是如果 scope.launch() 抛异常了,在它外面的 try-catch 可以捕获到。但对于协程运行阶段的代码,也就是 launch() 大括号里面的代码,它是运行在 Default 线程池的,与 try-catch 都不在一个线程中,因此无法被捕获。想要捕获协程运行阶段的代码,可以像示例代码 1 那样把 try-catch 放在协程内部。
那么真的就没办法在协程外部捕获协程内部的异常了吗?答案是有,可以使用 CoroutineExceptionHandler,将其对象传给最外层协程:
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)val handler = CoroutineExceptionHandler { _, exception ->println("Caught $exception")}// 这个 try-catch 无法捕获到 launch 内的异常try {scope.launch(handler) {throw RuntimeException()}} catch (e: Exception) {}delay(10000)
}
运行输出结果,异常没有抛出:
Caught java.lang.RuntimeException
使用 CoroutineExceptionHandler 一定要注意的是,CoroutineExceptionHandler 对象只能设置给最外层协程,这样最外层协程本身及其所有子协程所抛出的异常才可被捕获。如果设置给内层的某个子协程,即便是该子协程抛出异常也无法被捕获:
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)val handler = CoroutineExceptionHandler { _, exception ->println("Caught $exception")}scope.launch(/*handler*/) {// 这样还是会抛异常launch(handler) {throw RuntimeException()}}delay(10000)
}
3、异常结构化管理的本质
3.1 UncaughtExceptionHandler
捕获线程异常,除了 try-catch,还可以使用 UncaughtExceptionHandler:
fun main() = runBlocking<Unit> {val thread = Thread {throw RuntimeException("Thread error!")}// 在线程启动之前设置 UncaughtExceptionHandlerthread.setUncaughtExceptionHandler { _, e -> println("Caught $e") }thread.start()
}
在线程启动之前,可以调用 setUncaughtExceptionHandler() 给该线程设置异常捕获,运行结果如下:
Caught java.lang.RuntimeException: Thread error!
除了给单个线程设置,也可以给所有线程设置默认的 UncaughtExceptionHandler:
fun main() = runBlocking<Unit> {Thread.setDefaultUncaughtExceptionHandler { _, e -> println("Default caught: $e") }val thread = Thread {throw RuntimeException("Thread error!")}thread.setUncaughtExceptionHandler { _, e -> println("Caught $e") }thread.start()thread {throw IllegalStateException("Wrong User!")}
}
运行结果:
Caught java.lang.RuntimeException: Thread error!
Default caught: java.lang.IllegalStateException: Wrong User!
结果显示,对于单独设置了 UncaughtExceptionHandler 的线程,会使用单独设置的 UncaughtExceptionHandler 捕获异常。未设置的线程会使用默认的 UncaughtExceptionHandler 捕获异常。
对于异常处理要有一些深入的思考。比如对于 Android 应用而言,发生未捕获的异常会发生 FC 导致应用崩溃关闭。为了避免 FC 的发生,在写代码的时候,对于可能发生异常的代码,我们会用 try-catch 把它包起来,在 catch 代码块中做一些补救工作,比如尝试解决问题(重新执行函数或者重置变量值等等)或重启应用。而不是单单只为了通过 catch 吞掉这个异常而不发生 FC,因为即便不 FC,抛异常也意味着你的应用没有正常工作。盲目的吞掉异常让软件在异常状态下继续运行,可能会产生无法预料的结果。
对于能提前预料的异常,可以使用 try-catch。但是往往会有没有预判的异常出现,跑出 catch 的捕获,最终被 UncaughtExceptionHandler 接收到。此时线程已经运行结束了,没有机会做补救工作了,只能做一些善后工作,使用通用的解决方案优雅地结束应用。一般有两步:收尾工作(如记录崩溃日志)以及重启或杀死应用:
Thread.setDefaultUncaughtExceptionHandler { _, e ->// 记录崩溃日志println("Default caught: $e")// 结束或重启应用exitProcess(1)
}
对于为单个线程设置的 UncaughtExceptionHandler,如果该线程只是在内部完成自己的工作,其崩溃不影响其他线程,可以尝试在 UncaughtExceptionHandler 中重启线程。当然,一个线程独立完成一件事的场景比较少见。
3.2 CoroutineExceptionHandler
让我们再来看协程。如果没有捕获协程的异常,它最终会抛到线程的环境中,通过默认的 UncaughtExceptionHandler 可以捕获协程抛出的异常:
fun main() = runBlocking<Unit> {// 协程异常会被线程的 DefaultUncaughtExceptionHandler 捕获Thread.setDefaultUncaughtExceptionHandler { _, e ->println("Caught in DefaultUncaughtExceptionHandler: $e")}val scope = CoroutineScope(EmptyCoroutineContext)// 协程没有捕获异常val job = scope.launch {launch {throw RuntimeException()}}job.join()
}
使用 CoroutineExceptionHandler 为 job 这个协程树添加异常捕获处理,可以实现类似于为单个线程设置 UncaughtExceptionHandler 的效果:
fun main() = runBlocking<Unit> {Thread.setDefaultUncaughtExceptionHandler { _, e ->println("Caught in DefaultUncaughtExceptionHandler: $e")}val scope = CoroutineScope(EmptyCoroutineContext)val handler = CoroutineExceptionHandler { _, exception ->println("Caught in CoroutineExceptionHandler: $exception")}val job = scope.launch(handler) {launch {throw RuntimeException()}}job.join()
}
异常会被 CoroutineExceptionHandler 捕获:
Caught in CoroutineExceptionHandler: java.lang.RuntimeException
上一节我们说,很多任务都是通过多个线程完成的,因此在单个线程的 UncaughtExceptionHandler 做重启线程的适用场景并不多。但是我们看 CoroutineExceptionHandler 正是为一个协程树做异常善后工作所用的,将一个任务放在协程树中去执行,当某一个协程发生异常后,可以在 CoroutineExceptionHandler 中重启整个协程树,这就具有更广泛的实际意义了。
当然,CoroutineExceptionHandler 并不能替代 DefaultUncaughtExceptionHandler 去对整个应用做未知异常的善后工作,它只能针对一棵协程树。即便是纯协程应用,也要通过 DefaultUncaughtExceptionHandler 来做通用拦截。
现在再来考虑,子协程的异常为什么要交到最外层父协程那里去注册 CoroutineExceptionHandler?因为注册 CoroutineExceptionHandler 的目的是善后,不管是哪个协程发生了异常,都是对整个协程树进行善后。因此设置给最外层协程最方便。
协程异常的结构化管理的本质,是针对协程发生的未知异常的善后方案。因为已知异常,直接通过在协程内部 try-catch 就可以修复,只有未知异常才会走到结构化异常处理流程。
4、async 的异常处理
async 的异常处理与 launch 的大致相同,但是因为 async 启动的协程,往往需要通过 await 获取结果,会有两点比较明显的差异:
- 如果在 async 内发生异常,那么调用 await 的协程会受到双重影响
- async 启动的协程在 await 抛出异常,这个异常往往在协程内部就被 try-catch 捕获了。因此 async 作为最外层父协程存在时,不会将内部异常抛出到线程世界,给最外层的 async 设置的 CoroutineExceptionHandler 一般不会起作用
如果在
async块中发生了异常,这个异常会被包装在Deferred对象中,然后在调用await方法时会抛出这个异常。
先来看第一点,示例代码:
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)val handler = CoroutineExceptionHandler { _, exception ->println("Caught in CoroutineExceptionHandler: $exception")}val job = scope.launch(handler) {val deferred = async {delay(1000)throw RuntimeException()}launch {// await 也会抛出 async 内抛出的 RuntimeExceptiontry {deferred.await()} catch (e: Exception) {println("Caught in await: $e")}// delay 用来验证 async 抛出的异常触发了结构化取消,导致// async -> job -> 当前协程被取消try {delay(1000)} catch (e: Exception) {println("Caught in delay: $e")}}}job.join()
}
运行结果:
Caught in await: java.lang.RuntimeException
Caught in delay: kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=StandaloneCoroutine{Cancelling}@6463030a
Caught in CoroutineExceptionHandler: java.lang.RuntimeException
观察结果:
- 因为调用 await 时才会抛出 async 内抛出的异常,所以它的 try-catch 先捕获到异常
- 然后由于 async 抛的异常先让自己,也就是 deferred 被取消,进而导致父协程 job 被取消,进一步导致 delay 所在的协程也要被取消,所以 delay 抛出 CancellationException
- async 与 await 抛出的异常最终会被最外层协程的 CoroutineExceptionHandler 捕获
以上过程能看出,调用 await 的协程实际上受到双重影响:一是 await 会抛出与 async 抛出的异常导致该协程进入异常处理流程;二是 async 抛出异常使得父协程与兄弟协程都会被取消,await 所在协程还会触发取消流程。
再看第二点差异,示例代码:
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)val handler = CoroutineExceptionHandler { _, exception ->println("Caught in CoroutineExceptionHandler: $exception")}scope.async {val deferred = async {delay(1000)throw RuntimeException("Error!")}launch(Job()) {try {deferred.await()} catch (e: Exception) {println("Caught in await: $e")}}}delay(3000)
}
由于 await 抛异常会被 try-catch 捕获,因此 async 就不会向线程世界抛出异常,此时给 async 设置 CoroutineExceptionHandler 是多余的。但倘若没有为 await 添加 try-catch,其异常还是会被最外层的 async 抛到线程世界,需要 CoroutineExceptionHandler:
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)val handler = CoroutineExceptionHandler { _, exception ->println("Caught in CoroutineExceptionHandler: $exception")}scope.async(handler) {val deferred = async {delay(1000)throw RuntimeException("Error!")}launch(Job()) {deferred.await()}}delay(3000)
}
运行结果:
Caught in Coroutine: java.lang.RuntimeException: Error!
5、SupervisorJob
5.1 SupervisorJob 的作用
SupervisorJob 源码:
@Suppress("FunctionName")
public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {// 子协程被取消,父协程会调用 childCancelledoverride fun childCancelled(cause: Throwable): Boolean = false
}
子协程被取消,父协程会调用 childCancelled()。但对于 SupervisorJob 而言,它直接返回 false 表示子协程发生一般异常(非 CancellationException)时,不取消父协程:
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)val supervisorJob = SupervisorJob()scope.launch(supervisorJob) {throw RuntimeException("Error!")}delay(100)println("Parent job cancelled: ${supervisorJob.isCancelled}")delay(1000)
}
在抛出异常后仍然会输出:
Parent job cancelled: false
说明子协程的异常没有导致 supervisorJob 被取消。
5.2 SupervisorJob 的常用方式
使用 SupervisorJob 开发时一种常见的格式:
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)val supervisorJob = SupervisorJob()val job = scope.launch {// coroutineContext 是 scope 的,job 也就是 scope 内的 job 作为// SupervisorJob 的父协程,SupervisorJob 又作为这个 launch 启动// 协程的父协程,作为连接内外的链条launch(SupervisorJob(coroutineContext.job)) {throw RuntimeException("Error!")}}delay(100)println("Parent job cancelled: ${supervisorJob.isCancelled}")println("Job cancelled: ${job.isCancelled}")delay(1000)
}
让 SupervisorJob 作为作为 job 的子协程,还作为内层 launch 启动协程的父协程,也就是让 SupervisorJob 作为中间链条。这样内层协程发生异常时不会取消父协程,但 job 取消时会取消所有子协程:
Exception in thread "DefaultDispatcher-worker-2" java.lang.RuntimeException: Error!
...
Parent job cancelled: false
Job cancelled: false
还有一种常用方式是将全新的 SupervisorJob 直接传给 CoroutineScope,这样由该 CoroutineScope 启动的协程,其 SupervisorJob 不会因为异常而被取消:
fun main() = runBlocking<Unit> {val supervisorJob = SupervisorJob()val scope = CoroutineScope(supervisorJob)var child1: Job? = nullvar child2: Job? = nullval job = scope.launch {child1 = launch {child2 = launch() {throw RuntimeException("Error!")}}}delay(100)println("SupervisorJob cancelled: ${supervisorJob.isCancelled}")println("Job cancelled: ${job.isCancelled}")println("ChildJob1 cancelled: ${child1?.isCancelled}")println("ChildJob2 cancelled: ${child2?.isCancelled}")delay(1000)
}
运行结果:
Exception in thread "DefaultDispatcher-worker-3" java.lang.RuntimeException: Error!
...
SupervisorJob cancelled: false
Job cancelled: true
ChildJob1 cancelled: true
ChildJob2 cancelled: true
此外,SupervisorJob 的子协程内部抛出异常时,即便 SupervisorJob 的直接子协程不是最外层协程,也会由 SupervisorJob 将异常抛到线程中:
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)scope.launch {val handler = CoroutineExceptionHandler { _, exception ->println("Caught in handler: $exception")}launch(SupervisorJob(coroutineContext.job) + handler) {launch {throw RuntimeException("Error!")}}}delay(1000)
}
按照前面说过的,将异常抛到线程中的应该是最外层协程,但是在中间的协程设置 SupervisorJob 为父 Job 的情况下,是由该 launch 将异常抛出到线程中的:
Caught in handler: java.lang.RuntimeException: Error!
相关文章:
Kotlin 协程基础三 —— 结构化并发(二)
Kotlin 协程基础系列: Kotlin 协程基础一 —— 总体知识概述 Kotlin 协程基础二 —— 结构化并发(一) Kotlin 协程基础三 —— 结构化并发(二) Kotlin 协程基础四 —— CoroutineScope 与 CoroutineContext Kotlin 协程…...
微信小程序实现长按录音,点击播放等功能,CSS实现语音录制动画效果
有一个需求需要在微信小程序上实现一个长按时进行语音录制,录制时间最大为60秒,录制完成后,可点击播放,播放时再次点击停止播放,可以反复录制,新录制的语音把之前的语音覆盖掉,也可以主动长按删…...
校园跑腿小程序---轮播图,导航栏开发
hello hello~ ,这里是 code袁~💖💖 ,欢迎大家点赞🥳🥳关注💥💥收藏🌹🌹🌹 🦁作者简介:一名喜欢分享和记录学习的在校大学生…...
详细全面讲解C++中重载、隐藏、覆盖的区别
文章目录 总结1、重载示例代码特点1. 模板函数和非模板函数重载2. 重载示例与调用规则示例代码调用规则解释3. 特殊情况与注意事项二义性问题 函数特化与重载的交互 2. 函数隐藏(Function Hiding)概念示例代码特点 3. 函数覆盖(重写ÿ…...
一文读懂单片机的串口
目录 串口通信的基本概念 串口通信的关键参数 单片机串口的硬件连接 单片机串口的工作原理 数据发送过程 数据接收过程 单片机串口的编程实现 以51单片机为例 硬件连接 初始化串口 发送数据 接收数据 串口中断服务函数 代码示例 单片机串口的应用实例 单片机与…...
HTML5 网站模板
HTML5 网站模板 参考 HTML5 Website Templates...
mybatis分页插件:PageHelper、mybatis-plus-jsqlparser(解决SQL_SERVER2005连接分页查询OFFSET问题)
文章目录 引言I PageHelper坐标II mybatis-plus-jsqlparser坐标Spring Boot 添加分页插件自定义 Mapper 方法中使用分页注意事项解决SQL_SERVER2005连接分页查询OFFSET问题知识扩展MyBatis-Plus 框架结构mybatis-plus-jsqlparser的 Page 类引言 PageHelper import com.github.p…...
uniapp中rpx和upx的区别
在 UniApp 中,rpx 和 upx 是两种不同的单位,它们的主要区别在于适用的场景和计算方式。 ### rpx(Responsive Pixel) - **适用场景**:rpx 是一种响应式单位,主要用于小程序和移动端的布局。 - **计算方式**…...
什么是卷积网络中的平移不变性?平移shft在数据增强中的意义
今天来介绍一下数据增强中的平移shft操作和卷积网络中的平移不变性。 1、什么是平移 Shift 平移是指在数据增强(data augmentation)过程中,通过对输入图像或目标进行位置偏移(平移),让目标在图像中呈现出…...
java.net.SocketException: Connection reset 异常原因分析和解决方法
导致此异常的原因,总结下来有三种情况: 一、服务器端偶尔出现了异常,导致连接关闭 解决方法: 采用出错重试机制 二、 服务器端和客户端使用的连接方式不一致 解决方法: 服务器端和客户端使用相同的连接方式ÿ…...
Maven 仓库的分类
Maven 是一个广泛使用的项目构建和依赖管理工具,在 Java 开发生态中占据重要地位。作为 Maven 的核心概念之一,仓库(Repository)扮演着至关重要的角色,用于存储项目的依赖、插件以及构建所需的各种资源。 了解 Maven 仓…...
隧道网络:为数据传输开辟安全通道
什么是隧道网络? 想象一下,你正在一个陌生的城市旅行,并且想要访问家里的电脑。但是,直接连接是不可能的,因为家庭网络通常受到防火墙或路由器的保护,不允许外部直接访问。这时候,隧道网络&…...
CentOS 7 下 Nginx 的详细安装与配置
1、安装方式 1.1、通过编译方式安装 下载Nginx1.16.1的安装包 https://nginx.org/download/nginx-1.16.1.tar.gz 下载后上传至/home目录下。 1.2、通过yum方式安装 这种方式安装更简单。 2、通过编译源码包安装Nginx 2.1、安装必要依赖 sudo yum -y install gcc gcc-c sudo…...
JAVA 使用apache poi实现EXCEL文件的输出;apache poi实现标题行的第一个字符为红色;EXCEL设置某几个字符为别的颜色
设置输出文件的列宽,防止文件过于丑陋 Sheet sheet workbook.createSheet(FileConstants.ERROR_FILE_SHEET_NAME); sheet.setColumnWidth(0, 40 * 256); sheet.setColumnWidth(1, 20 * 256); sheet.setColumnWidth(2, 20 * 256); sheet.setColumnWidth(3, 20 * 25…...
通过vba实现在PPT中添加计时器功能
目录 一、前言 二、具体实现步骤 1、准备 2、开启宏、打开开发工具 3、添加计时器显示控件 3.1、开启母版 3.2、插入计时器控件 4、vba代码实现 4.1、添加模块 4.2、添加代码 4.3、保存为pptm 5、效果展示 一、前言 要求/目标:在PPT中每一页上面增加一个计时器功能…...
检验统计量与p值笔记
一、背景 以雨量数据为例,当获得一个站点一年的日雨量数据后,我们需要估计该站点的雨量的概率分布情况,因此我们利用有参估计的方式如极大似然法估计得到了假定该随机变量服从某一分布的参数,从而得到该站点的概率密度函数&#x…...
【集成学习】Bagging、Boosting、Stacking算法详解
文章目录 1. 相关算法详解:2. 算法详细解释:2.1 Bagging:2.2 Boosting:2.3 Stacking:2.4 K-fold Multi-level Stacking: 集成学习(Ensemble Learning)是一种通过结合多个模型的预测结…...
Rabbit Rocket kafka 怎么实现消息有序消费和延迟消费的
在消息队列系统中,像 RabbitMQ、RocketMQ 和 Kafka 这样的系统,都支持不同的方式来实现消息的有序消费和延迟消费。下面我们分别探讨这些系统中如何实现这两种需求: 1. RabbitMQ:实现消息有序消费和延迟消费 有序消费࿱…...
【Ubuntu与Linux操作系统:五、文件与目录管理】
第5章 磁盘存储管理 5.1 Linux磁盘存储概述 磁盘存储是Linux系统存储数据的重要组件,它通过分区和文件系统组织和管理数据。Linux支持多种文件系统,如ext4、xfs和btrfs,并以块的形式管理存储设备。 1. 分区与文件系统: 分区&am…...
32_Redis分片集群原理
1.Redis集群分片 1.1 Redis集群分片介绍 Redis集群没有使用一致性hash,而是引入了哈希槽的概念。Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽。 用于将密钥映射到散列插槽的基本算法如下: HASH_SLOT = CRC16(key) mod 16384 集群的每…...
Java 语言特性(面试系列2)
一、SQL 基础 1. 复杂查询 (1)连接查询(JOIN) 内连接(INNER JOIN):返回两表匹配的记录。 SELECT e.name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id d.dept_id; 左…...
synchronized 学习
学习源: https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖,也要考虑性能问题(场景) 2.常见面试问题: sync出…...
R语言AI模型部署方案:精准离线运行详解
R语言AI模型部署方案:精准离线运行详解 一、项目概述 本文将构建一个完整的R语言AI部署解决方案,实现鸢尾花分类模型的训练、保存、离线部署和预测功能。核心特点: 100%离线运行能力自包含环境依赖生产级错误处理跨平台兼容性模型版本管理# 文件结构说明 Iris_AI_Deployme…...
uni-app学习笔记二十二---使用vite.config.js全局导入常用依赖
在前面的练习中,每个页面需要使用ref,onShow等生命周期钩子函数时都需要像下面这样导入 import {onMounted, ref} from "vue" 如果不想每个页面都导入,需要使用node.js命令npm安装unplugin-auto-import npm install unplugin-au…...
FastAPI 教程:从入门到实践
FastAPI 是一个现代、快速(高性能)的 Web 框架,用于构建 API,支持 Python 3.6。它基于标准 Python 类型提示,易于学习且功能强大。以下是一个完整的 FastAPI 入门教程,涵盖从环境搭建到创建并运行一个简单的…...
将对透视变换后的图像使用Otsu进行阈值化,来分离黑色和白色像素。这句话中的Otsu是什么意思?
Otsu 是一种自动阈值化方法,用于将图像分割为前景和背景。它通过最小化图像的类内方差或等价地最大化类间方差来选择最佳阈值。这种方法特别适用于图像的二值化处理,能够自动确定一个阈值,将图像中的像素分为黑色和白色两类。 Otsu 方法的原…...
微服务商城-商品微服务
数据表 CREATE TABLE product (id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 商品id,cateid smallint(6) UNSIGNED NOT NULL DEFAULT 0 COMMENT 类别Id,name varchar(100) NOT NULL DEFAULT COMMENT 商品名称,subtitle varchar(200) NOT NULL DEFAULT COMMENT 商…...
浅谈不同二分算法的查找情况
二分算法原理比较简单,但是实际的算法模板却有很多,这一切都源于二分查找问题中的复杂情况和二分算法的边界处理,以下是博主对一些二分算法查找的情况分析。 需要说明的是,以下二分算法都是基于有序序列为升序有序的情况…...
论文笔记——相干体技术在裂缝预测中的应用研究
目录 相关地震知识补充地震数据的认识地震几何属性 相干体算法定义基本原理第一代相干体技术:基于互相关的相干体技术(Correlation)第二代相干体技术:基于相似的相干体技术(Semblance)基于多道相似的相干体…...
算法:模拟
1.替换所有的问号 1576. 替换所有的问号 - 力扣(LeetCode) 遍历字符串:通过外层循环逐一检查每个字符。遇到 ? 时处理: 内层循环遍历小写字母(a 到 z)。对每个字母检查是否满足: 与…...
