Coroutine 基础四 —— CoroutineScope 与 CoroutineContext
1、定位
CoroutineContext,协程上下文。协程用到的所有信息都属于协程上下文,根据功能不同,划分成了不同的分类。管理流程用的是 Job,管理线程用的是 ContinuationInterceptor,等等。
CoroutineScope 的定位有两点:
- CoroutineScope 是 CoroutineContext 的容器,因此 CoroutineScope 可以提供对应协程的上下文信息
- CoroutineScope 内还定义了 launch 和 async 两个启动协程的函数,因此可以用来启动协程
2、GlobalScope
GlobalScope 是一个特殊的 CoroutineScope:
@DelicateCoroutinesApi
public object GlobalScope : CoroutineScope {/*** Returns [EmptyCoroutineContext].*/override val coroutineContext: CoroutineContextget() = EmptyCoroutineContext
}
特殊之处:
- GlobalScope 是一个单例
- GlobalScope 没有内置的 Job
其他的 CoroutineScope 都会有一个内置的 Job:
@Suppress("FunctionName")
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =ContextScope(if (context[Job] != null) context else context + Job())
但是 GlobalScope 没有:
fun main() = runBlocking<Unit> {println(GlobalScope.coroutineContext[Job])val job = GlobalScope.launch { // this:CoroutineContextprintln(coroutineContext[Job])}println("job parent: ${job.parent}")delay(100)
}
运行结果:
null
StandaloneCoroutine{Active}@488e7dc8
job parent: null
GlobalScope 没有内置 Job,导致由其启动的协程,也就是 job 没有父协程。
GlobalScope 可以让开发者不用自己创建 CoroutineScope 也能启动协程。很多协程都是有绑定的生命周期的,比如某些协程与界面绑定生命周期,当界面关闭时,我们希望协程也自动取消。我们就可以用与界面生命周期绑定的 CoroutineScope(如 MainScope、LifecycleScope)来启动协程。但有时候,协程的生命周期是不跟任何应用组件绑定的,或者说它要一直存活到应用程序结束。此时就可以使用 GlobalScope 来启动协程。GlobalScope 启动的所有协程之间,与 GlobalScope 也没有任何的关联(取消、异常相互没有影响)。
3、从挂起函数里获取 CoroutineContext
我们在协程内很容易的就能通过 CoroutineScope 获取 CoroutineContext:
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)val job = scope.launch {println("Dispatcher: ${coroutineContext[ContinuationInterceptor]}")}job.join()
}
但是,如果把这个工作抽取到挂起函数中,由于挂起函数没有 CoroutineScope 类型的 this,因此它只能声明为 CoroutineScope 的扩展函数才能拿到 CoroutineContext:
private suspend fun CoroutineScope.showDispatcher() {println("Dispatcher: ${coroutineContext[ContinuationInterceptor]}")
}
但是,由于挂起函数最终都是要在协程中运行的,因此它是可以通过协程的 CoroutineScope 访问到 CoroutineContext 的,这是一个合理诉求。那也不能对所有要访问 CoroutineContext 的挂起函数都声明为 CoroutineScope 的扩展函数吧?这时候 Kotlin 出手了,为挂起函数加了一个属性 kotlin.coroutines.coroutineContext
,需要手动导入:
private suspend fun showDispatcher() {delay(100)println("Dispatcher: ${coroutineContext[ContinuationInterceptor]}")
}
该属性的 get() 是一个挂起函数:
@SinceKotlin("1.3")
@Suppress("WRONG_MODIFIER_TARGET")
@InlineOnly
public suspend inline val coroutineContext: CoroutineContext// get 的实现在别的位置,不是一直都抛异常get() {throw NotImplementedError("Implemented as intrinsic")}
除此之外,还有一个函数 currentCoroutineContext() 可以返回这个 coroutineContext:
public suspend inline fun currentCoroutineContext(): CoroutineContext = coroutineContext
该函数用于在特殊情况下避免命名冲突。这个特殊情况是在协程中调用 Flow,并且 Flow 的参数中需要访问 coroutineContext 的时候。
flow() 的参数是一个挂起函数:
public fun <T> flow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit): Flow<T> = SafeFlow(block)
在这个挂起函数中可以访问到 coroutineContext 属性:
private fun flowFun() {flow<String> {coroutineContext}
}
但是假如在协程中使用 flow:
private fun flowFun() {flow<String> {// kotlin.coroutines.coroutineContextcoroutineContext}GlobalScope.launch {flow<String> {// CoroutineScope 内的 coroutineContext 属性coroutineContext}}
}
协程内的 coroutineContext 指向 CoroutineScope 内的 coroutineContext:
public interface CoroutineScope {public val coroutineContext: CoroutineContext
}
这是因为 flow 的大括号内部实际上是一个双重环境:
GlobalScope.launch { // this:CoroutineScopeflow<String> { // 挂起函数的环境// CoroutineScope 内的 coroutineContext 属性coroutineContext}
}
双重环境是指协程提供的 CoroutineScope 以及 flow 参数的挂起函数的环境,两个环境都有 coroutineContext 这个变量,但两个变量并不是同一个对象,也就是名字相同但对象不同,发生了命名冲突。前者是 CoroutineScope 接口内的 coroutineContext 属性,后者是 kotlin.coroutines.coroutineContext
这个顶级属性。成员属性的优先级比顶级属性的优先级更高,因此最终拿到的就是 CoroutineScope 接口中的属性。
为了解决这个问题,Kotlin 才提供了 currentCoroutineContext() 帮助我们拿到那个顶级的 coroutineContext 属性。
4、coroutineScope() 与 supervisorScope()
coroutineScope() 的作用在其注释上已经明确写出:创建一个 CoroutineScope 并且通过这个 Scope 调用指定的挂起代码块。该 Scope 会从外部的 Scope 中继承 coroutineContext,使用该 coroutineContext 的 Job 作为新启动 Job 的父 Job。
这个功能描述跟 launch 非常像,都是启动一个子协程,并且协程内部的 coroutineContext 从外部继承,同时在协程内部创建一个新的 Job。
但是二者的区别很关键,也导致它俩的功能定位不同:
- coroutineScope() 没有参数,因此无法通过参数定制 CoroutineContext;但 launch() 有参数,可以指定 ContinuationInterceptor,也可以指定父 Job
- coroutineScope() 是挂起函数,会等内部代码执行完毕再返回,即与 coroutineScope() 所在协程是串行关系;而 launch() 只是协程的启动器,启动协程后 launch 也就执行完了,被启动的协程与 launch 所在的协程是并行关系
示例代码:
fun main() = runBlocking<Unit> {val startTime = System.currentTimeMillis()coroutineScope {delay(1000)println("Duration within coroutineScope: ${System.currentTimeMillis() - startTime}")// 假如内部启动了多个子协程,coroutineScope 启动的这个父协程会等待它们执行完毕}println("Duration of coroutineScope: ${System.currentTimeMillis() - startTime}")val startTime1 = System.currentTimeMillis()val job = launch {delay(1000)println("Duration within launch: ${System.currentTimeMillis() - startTime1}")}println("Duration of launch: ${System.currentTimeMillis() - startTime1}")job.join()
}
运行结果:
Duration within coroutineScope: 1013
Duration of coroutineScope: 1021
Duration of launch: 2
Duration within launch: 1007
结果表明 coroutineScope() 确实是串行的。
coroutineScope() 最常用的场景是在挂起函数里提供一个 CoroutineScope 的上下文:
private suspend fun someFun() {coroutineScope { launch { }}
}
假如想在挂起函数中用 launch/async 启动协程,由于挂起函数内没有 CoroutineScope 类型的 this,因此需要有一个东西能提供,这个东西就是 coroutineScope()。
不要想着为什么不用 CoroutineScope 的扩展函数:
private suspend fun CoroutineScope.someFun() {launch { }
}
因为上一节说过,这样做会造成 coroutineContext 的命名冲突。
coroutineScope() 与 launch() 还有一个不是很重要的区别:coroutineScope() 的返回值是代码块内的最后一行,而 launch() 的返回值是 Job。这会引发 coroutineScope() 的第二个应用场景,对串行模块的封装。
利用这一点,可以使用 coroutineScope() 直接返回 async 的运行结果:
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)val job = scope.launch {val result = coroutineScope {val deferred = async { "Coroutine" }deferred.await()}println("result: $result")}job.join()
}
进一步延伸,可以使用 coroutineScope() 封装业务代码:
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)val job = scope.launch {val result = coroutineScope {// 业务代码,举个简单例子val deferred1 = async { "Coroutine" }val deferred2 = async { "Scope" }deferred1.await() + deferred2.await()}println("result: $result")}job.join()
}
这样封装的好处是,在 coroutineScope() 外使用 try-catch 就可以捕获到 coroutineScope() 内协程抛出的异常,不至于影响 coroutineScope() 所在的整棵协程树:
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)val job = scope.launch {val result = try {coroutineScope {val deferred1 = async { "Coroutine" }// 假如这个协程抛异常了val deferred2 = async { throw RuntimeException("Error!") }deferred1.await() + deferred2.await()}} catch (e: Exception) {e.message}println("result: $result")}job.join()
}
运行结果:
result: Error!
能对 coroutineScope() 进行 try-catch 的原因是它是一个挂起函数,当内部协程运行时,该函数处于挂起状态,如果内部协程发生异常,外部协程是可以感知到的。
因此可以使用 coroutineScope() 封装业务代码,这个在前面讲 async 的时候也介绍过,假如在一大段代码中,有一小部分代码是通过多个子协程去执行某一个子流程的,通常会使用 coroutineScope() 把它们括起来。这样即便这些子协程发生了异常,你也可以通过 try-catch 进行异常处理,而不至于让整个协程树都被毁掉。
supervisorScope() 里面用的是类似于 SupervisorJob 的 Job(实质上不是 SupervisorJob,但是功能完全一样)。
5、再谈 withContext()
withContext() 与 coroutineScope() 的唯一不同在于,withContext() 允许填参数:
// CoroutineScope.kt:
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {// 确保 block 只被调用一次contract {callsInPlace(block, InvocationKind.EXACTLY_ONCE)}return suspendCoroutineUninterceptedOrReturn { uCont ->val coroutine = ScopeCoroutine(uCont.context, uCont)coroutine.startUndispatchedOrReturn(coroutine, block)}
}
public suspend fun <T> withContext(context: CoroutineContext,block: suspend CoroutineScope.() -> T
): T {contract {callsInPlace(block, InvocationKind.EXACTLY_ONCE)}return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->// compute new contextval oldContext = uCont.context// Copy CopyableThreadContextElement if necessaryval newContext = oldContext.newCoroutineContext(context)// always check for cancellation of new context// 验证新的上下文是否已经被取消了,如果是就抛 CancellationException 进入取消流程 newContext.ensureActive()// FAST PATH #1 -- new context is the same as the old one// 如果新旧协程上下文相同,那么后续执行的 return 部分与 coroutineScope() 内的 return 相同if (newContext === oldContext) {val coroutine = ScopeCoroutine(newContext, uCont)return@sc coroutine.startUndispatchedOrReturn(coroutine, block)}// FAST PATH #2 -- the new dispatcher is the same as the old one (something else changed)// `equals` is used by design (see equals implementation is wrapper context like ExecutorCoroutineDispatcher)if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) {val coroutine = UndispatchedCoroutine(newContext, uCont)// There are changes in the context, so this thread needs to be updatedwithCoroutineContext(coroutine.context, null) {return@sc coroutine.startUndispatchedOrReturn(coroutine, block)}}// SLOW PATH -- use new dispatcherval coroutine = DispatchedCoroutine(newContext, uCont)block.startCoroutineCancellable(coroutine, coroutine)coroutine.getResult()}
}
看 FAST PATH #1 部分,如果 withContext() 填的 context 参数没有改变 oldContext,比如 EmptyCoroutineContext,那么 withContext() 与 coroutineScope() 的运行效果是几乎一样的(除了 ensureActive())。
再看 FAST PATH #2,如果新旧上下文的 ContinuationInterceptor 没变,就用 UndispatchedCoroutine 启动一个新协程。
最后,SLOW PATH 就是指,如果线程都改变了,就用 DispatchedCoroutine 启动一个新协程。
所以 withContext() 可以看作是一个可以定制 CoroutineContext 的 coroutineScope()。另一个角度,它还可以看作是串行的 launch() 或 async()。
具体开发时,withContext() 的使用场景就是临时切换 CoroutineContext 的。
withContext() 虽然在底层还是开了协程,但是在上层,我们通常会认为它就是串行的切换了一个线程环境。
6、CoroutineName
fun main() = runBlocking<Unit> {val name = CoroutineName("MyCoroutine")// 1.给单个协程指定 CoroutineNamelaunch(name) {// 两种获取 CoroutineName 的方式println("CoroutineName: ${coroutineContext[CoroutineName]}")println("CoroutineName: ${coroutineContext[CoroutineName]?.name}")}// 2.给 CoroutineScope 指定 CoroutineScope,由该 CoroutineScope 启动的所有协程默认用这个名字val scope = CoroutineScope(Dispatchers.IO + name)scope.launch {println("CoroutineName: ${coroutineContext[CoroutineName]}")}delay(1000)
}
运行结果:
CoroutineName: CoroutineName(MyCoroutine)
CoroutineName: CoroutineName(MyCoroutine)
CoroutineName: MyCoroutine
此功能多用于测试与调试时,为想要监测的协程设置一个易于查看的名称。
7、CoroutineContext 的加减和 get()
7.1 +
+
是运算符重载,调用的是 CoroutineContext 的 plus():
public operator fun plus(context: CoroutineContext): CoroutineContext =if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creationcontext.fold(this) { acc, element ->val removed = acc.minusKey(element.key)if (removed === EmptyCoroutineContext) element else {// make sure interceptor is always last in the context (and thus is fast to get when present)val interceptor = removed[ContinuationInterceptor]if (interceptor == null) CombinedContext(removed, element) else {val left = removed.minusKey(ContinuationInterceptor)if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) elseCombinedContext(CombinedContext(left, element), interceptor)}}}
plus() 返回的 CombinedContext 会将两个 CoroutineContext 合并成一个:
internal class CombinedContext(private val left: CoroutineContext,private val element: Element
) : CoroutineContext, Serializable
Element 也是 CoroutineContext 的一个子接口:
public interface Element : CoroutineContext
比如进行如下操作时:
fun main() = runBlocking<Unit> {val scope = CoroutineScope(Dispatchers.IO + Job())println("$scope")
}
输出:
CoroutineScope(coroutineContext=[JobImpl{Active}@24273305, Dispatchers.IO])
然后再给 CoroutineScope 添加一个 CoroutineName:
fun main() = runBlocking<Unit> {val scope = CoroutineScope(Dispatchers.IO + Job() + CoroutineName("MyCoroutine"))println("$scope")
}
输出如下:
CoroutineScope(coroutineContext=[JobImpl{Active}@5a42bbf4, CoroutineName(MyCoroutine), Dispatchers.IO])
如果有多个相同类型的对象相加,那么新的会替换掉旧的:
fun main() = runBlocking<Unit> {val job1 = Job()val job2 = Job()val scope = CoroutineScope(Dispatchers.IO + job1 + CoroutineName("MyCoroutine") + job2)println("job1: $job1, job2: $job2")println("$scope")
}
运行结果:
job1: JobImpl{Active}@5d3411d, job2: JobImpl{Active}@2471cca7
CoroutineScope(coroutineContext=[CoroutineName(MyCoroutine), JobImpl{Active}@2471cca7, Dispatchers.IO])
通过哈希值判断,job2 替换了 job1。
需要注意,相同类型的 CoroutineContext 元素不能直接相加,比如 job1 + job2
这样编译器就会报错,因为 Job 在对加号进行重载时添加了限制:
@Deprecated(message = "Operator '+' on two Job objects is meaningless. " +"Job is a coroutine context element and `+` is a set-sum operator for coroutine contexts. " +"The job to the right of `+` just replaces the job the left of `+`.",level = DeprecationLevel.ERROR)public operator fun plus(other: Job): Job = other
两个 CoroutineName 和 CoroutineExceptionHandler 可以直接相加。
7.2 get()
coroutineContext[Job]
的中括号实际上是通过 get() 实现的:
public interface CoroutineContext {/*** Returns the element with the given [key] from this context or `null`.*/public operator fun <E : Element> get(key: Key<E>): E? // 得到 Key 的泛型类型对象/*** Key for the elements of [CoroutineContext]. [E] is a type of element with this key.*/public interface Key<E : Element>
}
get() 的参数传入的 Job 并不是指 Job 类型,而是 Job 接口的伴生对象 Key:
public interface Job : CoroutineContext.Element {/*** Key for [Job] instance in the coroutine context.*/public companion object Key : CoroutineContext.Key<Job>
}
由于 Kotlin 中访问一个类的伴生对象可以直接通过类名访问,比如 Job.Key,再进一步简写为只有类名 Job。因此 coroutineContext[Job]
实际上是调用 coroutineContext.get(Job)
,这个 Job 是 Job 类的伴生对象,也就是 Key,所以最终效果是 coroutineContext.get(Key)
,拿到的就是 Job 对象。
对于其他 CoroutineContext.Element 也是类似的,比如 CoroutineExceptionHandler:
public interface CoroutineExceptionHandler : CoroutineContext.Element {/*** Key for [CoroutineExceptionHandler] instance in the coroutine context.*/public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>...
}
以及 CoroutineName:
public data class CoroutineName(/*** User-defined coroutine name.*/val name: String
) : AbstractCoroutineContextElement(CoroutineName) {/*** Key for [CoroutineName] instance in the coroutine context.*/public companion object Key : CoroutineContext.Key<CoroutineName>...
}
还有 ContinuationInterceptor 等等。在获取 ContinuationInterceptor 时,可以直接用 coroutineContext[ContinuationInterceptor]
:
fun main() = runBlocking<Unit> {val interceptor = coroutineContext[ContinuationInterceptor]
}
但是如果想获取 ContinuationInterceptor 的子类 CoroutineDispatcher 对象时,可以使用强转的方式:
fun main() = runBlocking<Unit> {// 注意 get() 返回的是可空类型,因此不要遗忘了 ?val dispatcher: CoroutineDispatcher? =coroutineContext[ContinuationInterceptor] as CoroutineDispatcher?
}
或者直接通过 CoroutineDispatcher 的伴生对象:
@OptIn(ExperimentalStdlibApi::class)
fun main() = runBlocking<Unit> {val dispatcher: CoroutineDispatcher? = coroutineContext[CoroutineDispatcher]
}
但是必须加上 @OptIn(ExperimentalStdlibApi::class)
这个注解,因为 CoroutineDispatcher 的伴生对象 Key 有点不一样,它现在是有 @ExperimentalStdlibApi 注解的:
public abstract class CoroutineDispatcher :AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {/** @suppress */@ExperimentalStdlibApipublic companion object Key : AbstractCoroutineContextKey<ContinuationInterceptor, CoroutineDispatcher>(ContinuationInterceptor,{ it as? CoroutineDispatcher })}
8、自定义 CoroutineContext
协程提供了多种 CoroutineContext:ContinuationInterceptor、Job、CoroutineExceptionHandler、CoroutineName。
如果上述内容都不能满足要为 CoroutineContext 附加专属信息或专属功能的需求,就需要自定义 CoroutineContext。
自定义 CoroutineContext 一般无需直接实现 CoroutineContext,可以继承系统提供的半成品 AbstractCoroutineContextElement:
// public abstract class AbstractCoroutineContextElement(public override val key: Key<*>) : Element
class MyContext : AbstractCoroutineContextElement(MyContext) {companion object Key : CoroutineContext.Key<MyContext>// 自定义的功能suspend fun log() {println("Current coroutine: $coroutineContext")}
}
使用:
fun main() = runBlocking<Unit> {launch(MyContext()) {coroutineContext[MyContext]?.log()}delay(100)
}
运行结果:
Current coroutine: [com.kotlin.coroutine._3_scope_context.MyContext@61a52fbd, StandaloneCoroutine{Active}@33a10788, BlockingEventLoop@7006c658]
相关文章:
Coroutine 基础四 —— CoroutineScope 与 CoroutineContext
1、定位 CoroutineContext,协程上下文。协程用到的所有信息都属于协程上下文,根据功能不同,划分成了不同的分类。管理流程用的是 Job,管理线程用的是 ContinuationInterceptor,等等。 CoroutineScope 的定位有两点&a…...
科技云报到:洞见2025年科技潮流,技术大融合开启“智算时代”
科技云报到原创。 随着2024年逐渐接近尾声,人们不禁开始展望即将到来的2025年。这一年,被众多科技界人士视为开启新纪元的关键节点。站在新的起点上,我们将亲眼目睹未来科技如何改变我们的世界。从人工智能到量子计算,从基因编辑…...
MySQL有哪些锁?
1.MySQL有哪些锁? 全局锁表级锁 表锁元数据锁意向锁 行级锁 记录锁间隙锁临键锁临时意向锁 我了解的是MySQL的锁可以分为全局锁、表级锁、行级锁。 我比较熟悉的是表级锁和行级锁,如果我们对表结构进行修改时,MySQL就会对这个表结构加一个…...
通过交叉实现数据触底分页效果new IntersectionObserver()(html、react、vue2、vue3)中使用
react中用法 import React, { useState, useEffect, useRef } from react;const InfiniteScroll () > {const [items, setItems] useState([]);const [loading, setLoading] useState(false);const [page, setPage] useState(1);const loaderRef useRef(null);// 模拟…...
DeepSeek-V3-Base 模型技术解析
DeepSeek-V3-Base 模型技术解析 目录 引言DeepSeek-V3-Base 模型概述模型架构 3.1 Transformer 基础3.2 DeepSeek-V3-Base 的改进 训练过程 4.1 数据预处理4.2 训练策略4.3 优化器与学习率调度 模型性能评估 5.1 基准测试5.2 实际应用案例 模型优化与调参 6.1 超参数调优6.2 …...
Qt5 中 QGroupBox 标题下沉问题解决
我们设置了QGroupBox 样式之后,发现标题下沉了,那么如何解决呢? QGroupBox {font: 12pt "微软雅黑";color:white;border:1px solid white;border-radius:6px; } 解决后的效果 下面是解决方法: QGroupBox {font: 12pt "微软雅黑";color:white;bo…...
Linux Debian安装ClamAV和命令行扫描病毒方法,以及用Linux Shell编写了一个批量扫描病毒的脚本
ClamAV是一个开源的跨平台病毒扫描引擎,用于检测恶意软件、病毒、木马等安全威胁。 一、Linux Debian安装ClamAV 在Linux Debian系统上安装ClamAV,你可以按照以下步骤进行: 更新软件包列表: 打开终端并更新你的软件包列表&#…...
数据结构与算法之动态规划: LeetCode 53. 最大子数组和 (Ts版)
最大子数组和 https://leetcode.cn/problems/maximum-subarray/description/ 描述 给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和子数组是数组中的一个连续部分 示例 1 …...
活动预告 | Microsoft Azure 在线技术公开课:使用 Azure OpenAI 服务构建生成式应用
课程介绍 通过 Microsoft Learn 免费参加 Microsoft Azure 在线技术公开课,掌握创造新机遇所需的技能,加快对 Microsoft Cloud 技术的了解。参加我们举办的“使用 Azure OpenAI 服务构建生成式应用”活动,了解如何使用包括 GPT 在内的强大的…...
Python爬虫(二)- Requests 高级使用教程
文章目录 前言一、Session 对象1. 简介2. 跨请求保持 Cookie3. 设置缺省数据4. 方法级别参数不被跨请求保持5. 会话作为上下文管理器6. 移除字典参数中的值 二、请求与响应1. 请求与响应对象1.1 获取响应头信息1.2 获取发送到服务器的请求头信息 三、SSL 证书验证1. 忽略 SSL 证…...
协议幻变者:DeviceNet转ModbusTCP网关开启机器手臂智能新纪元
技术背景DeviceNet是一种广泛应用于工业自动化领域的现场总线标准,它能够实现控制器与现场设备之间的高效通信,常用于连接各种传感器、执行器以及其他工业设备,如机器人、电机驱动器等,具有实时性强、可靠性高的特点。而ModbusTCP…...
自定义有序Map
package cn.ziqirj.common.utils;import lombok.Getter; import lombok.Setter;import java.util.ArrayList; import java.util.List;/*** 模拟Map集合,key不可重复,按插入顺序排序* author zhangji** param <T>*/ public class CustomOrderlyMap&…...
CA系统的设计(CA证书生成,吊销,数字签名生成)
CA系统概述 CA认证系统是一种基于公钥密码基础设施(PKI)的信息安全技术,它可以为网络通信双方提供身份认证、数据加密、数字签名等功能。CA认证系统的核心是证书授权机构(CA),它负责为用户(节点…...
流计算需要框架吗?SPL 可能是更好的选择
流数据源通常是动态、无界的,看起来与静态、有限的批数据源区别较大,传统的数据库技术在架构上难以直接处理流数据源,只能让位于后来者。heron\samza\storm\spark\flink等计算框架最先完成突破,在流计算技术中占得先发优势。这些框…...
Vue-Router之嵌套路由
在路由配置中,配置children import Vue from vue import VueRouter from vue-routerVue.use(VueRouter)const router new VueRouter({mode: history,base: import.meta.env.BASE_URL,routes: [{path: /,redirect: /home},{path: /home,name: home,component: () &…...
MySQL 读写分离
MySQL 读写分离 一、配置主库(Master) 1.修改主库的配置文件 修改主库的 my.cnf 配置文件,生成二进制日志 (binary log) 和服务器唯一ID,这是实现主从复制的必要配置 [mysqld] # skip-grant-tables userroot port3306 basedir/usr/local/mysql datad…...
记一次音频无输出的解决方案
啊啊啊,刷个抖音就发现个死电脑死都不出声,捣鼓了一天才解决 打开wav文件时,提示错误找不到音频播放设备 0xc00d36fa 起初以为是声卡坏了,就到官网下载、更新了声卡驱动。无用什么驱动精灵也检测了,但也测不出啥来。…...
3D数学基础2
矩阵的行列式 在任意方阵中都存在至少一个标量,称作该方阵的行列式。在线性代数中,行列式有很多有用的性质 线性运算法则 方阵 M M M的行列式记作 ∣ M ∣ |M| ∣M∣或“det M”。非方阵矩阵的行列式是未定义的。 注意,在书写行列式时&…...
Java开发生态2024年度总结报告
1 关键要点 尽管数据显示 Java 17 是最常用 JDK,但其用户占比并未超过半数。根据 New Relic 2024 Java 生态系统状态报告,Java 17、11 和 8 的用户比例分别为 35%、33% 和 29%。New Relic 数据中所谓“快速采用”指 Java 21 的采用率仅为 1.4%。虽相较 J…...
1月第三讲:Java子线程无法获取Attributes的解决方法
在Java多线程编程中,开发者经常会遇到子线程无法获取主线程设置的Attributes的问题。Attributes通常用于存储与当前线程相关的数据,尤其在Web应用中,它们常用于请求上下文的管理。然而,由于Java线程是独立运行的,每个线…...
更新金碟云星空单据供应商和币别
--应付单 select FSUPPLIERID from [dbo].[T_AP_PAYABLE] where FBILLNO=AP2024121670 select FSUPPLIERID,FCURRENCYID,* from [dbo].[T_AP_PAYABLE] where FBILLNO=AP2024121670 -- update T_AP_PAYABLE set FSUPPLIERID=100567 where FBILLNO=AP2024121670 -- update T_…...
from memory cache 修复记录
背景 浏览器的页签图标,不想要了 改代码:设置浏览器页签的代码 本地环境测试,没有问题,一次性修改成功 于是打包,部署到测试环境,然而,还是有 接下的解决方法: 1、清除浏览器缓…...
spring入门程序
安装eclipse https://blog.csdn.net/qq_36437991/article/details/131644570 新建maven项目 安装依赖包 pom.xml <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation&quo…...
用于实现无缝滚动效果的vue-seamless-scroll插件
它通常用于在网页或应用中实现内容的自动滚动效果,如新闻公告、图片轮播等,支持横向和纵向滚动,并且可以自定义滚动速度、方向等参数,适合展示一些需要持续循环展示的信息。 在Vue2项目中使用vue-seamless-scroll插件的步骤如下&…...
借助 FinClip 跨端技术探索鸿蒙原生应用开发之旅
在当今数字化浪潮汹涌澎湃的时代,移动应用开发领域正经历着深刻的变革与创新。鸿蒙操作系统的崛起,以其独特的分布式架构和强大的性能表现,吸引了众多开发者的目光。而FinClip 跨端技术的出现,为开发者涉足鸿蒙原生应用开发提供了…...
【机器学习】机器学习的基本分类-自监督学习-对比学习(Contrastive Learning)
对比学习是一种自监督学习方法,其目标是学习数据的表征(representation),使得在表征空间中,相似的样本距离更近,不相似的样本距离更远。通过设计对比损失函数(Contrastive Loss)&…...
python之eval函数
功能:将字符串str当成有效的表达式来求值并返回计算结果 语法:eval(source,[,globals[,locals]])->value 参数: source:一个python表达式或函数compile()返回的代码对象globals:可选。必须是dictionarylocals&am…...
NXP i.MX8系列平台开发讲解 - 5.3 调试篇(二) - 掌握Dynamic debug调试
专栏文章目录传送门:返回专栏目录 Hi, 我是你们的老朋友,主要专注于嵌入式软件开发,有兴趣不要忘记点击关注【码思途远】 文章目录 目录 掌握Dynamic debug调试 1. 认识Dynamic debug 2. 内核配置 3. 使用Dynamic debug 3.1 查看当前的…...
QT----------常用界面组件的使用
一、QComboBox 类 主要功能:提供一个下拉列表,用户可以从中选择一个或多个选项。 #include <QApplication> #include <QComboBox> #include <QVBoxLayout> #include <QWidget> #include <QMessageBox>int main(int argc…...
重新整理机器学习和神经网络框架
本篇重新梳理了人工智能(AI)、机器学习(ML)、神经网络(NN)和深度学习(DL)之间存在一定的包含关系,以下是它们的关系及各自内容,以及人工智能领域中深度学习分支对比整理。…...
AJAX详解
AJAX是前后端交互的重要工具 结合前后端交互基础理解:前后端交互详解(建议收藏)-CSDN博客 1. AJAX - 到底什么是Ajax? ajax 全名 async javascript and XML(异步JavaScript和XML),是一种用于向服务器异步发送 HTTP 请求并接收响应的技术。 XML 指可扩…...
golang中的异常处理机制
今天是2024最后一天,祝大家新年梦想成真,继续我的魅力golang,昨天发的错误处理,是明显的可预见、可恢复的问题,然而,不可预见的问题,往往更多,golang也有自己的一套,完全…...
HTML5 开关(Toggle Switch)详细讲解
HTML5 开关(Toggle Switch)详细讲解 1. 任务概述 开关(Toggle Switch)是一种用于表示二元状态(如开/关)的用户界面控件。用户可以通过点击开关来切换状态,常见于设置选项、开关功能等场景。 2…...
【前端】Node.js使用教程
目录 一、?Node.js开发环境和编译 1.1 安装Node.js 1.2 创建一个Node.js项目 1.3 编写Node.js程序 1.4 运行Node.js程序 1.5 使用Node.js模块 二、高级的Node.js编程概念和示例 2.1 异步编程 2.2 错误处理 2.3 网络请求 2.4 构建Web服务器 2.5 数据库交互 三、No…...
在CodeBlocks搭建SDL2工程构建TFT彩屏模拟器虚拟TFT彩屏幕显示
在CodeBlocks搭建SDL2工程构建TFT彩屏模拟器虚拟TFT彩屏幕显示 参考文章源码下载地址一、SDL2的创建、初始化、退出二、系统基本Tick、彩屏刷新、按键事件三、彩屏获取与设置颜色四、彩屏填充颜色及清屏五、彩屏显示中文和英文字符串六、彩屏显示数字七、彩屏初始化八、主函数测…...
BurstAttention:高效的分布式注意力计算框架
BurstAttention:高效的分布式注意力计算框架 在现代大型语言模型(LLMs)的应用中,提升注意力机制的计算效率已成为研究的热点。当前,提升计算效率主要有两种方法:一种是优化单设备的计算和存储能力…...
sentinel集成nacos启动报[check-update] get changed dataId error, code: 403错误排查及解决
整合nacos报403错误 因为平台写的一个限流代码逻辑有问题,所以准备使用sentinel来限流。平台依赖里面已经引入了,之前也测试过,把sentinel关于nacos的配置加上后,启动一直输出403错误 [fixed-10.0.20.188_8848-test] [check-upda…...
[TOTP]android kotlin实现 totp身份验证器 类似Google身份验证器
背景:自己或者公司用一些谷歌身份验证器或者microsoft身份验证器,下载来源不明,或者有广告,使用不安全。于是自己写一个,安全放心使用。 代码已开源:shixiaotian/sxt-android-totp: android totp authenti…...
IDEA+Docker一键部署项目SpringBoot项目
文章目录 1. 部署项目的传统方式2. 前置工作3. SSH配置4. 连接Docker守护进程5. 创建简单的SpringBoot应用程序6. 编写Dockerfile文件7. 配置远程部署 7.1 创建配置7.2 绑定端口7.3 添加执行前要运行的任务 8. 部署项目9. 开放防火墙的 11020 端口10. 访问项目11. 可能遇到的问…...
【发票提取明细+发票号改名】批量提取PDF电子发票明细导出Excel表格并改名技术难点,批量PDF多区域内容识别提取明细并用内容改名的小结
1、图片版的发票提取表格改名 【批量图片发票识别表格】批量图片发票的提取Excel表格和提取字段改名,扫描发票识别表格,拍照发票识别表格,图片发票识别改名我们在工作中很多扫描发票,拍照发票,需要整理成excel表格&am…...
pyQT + OpenCV相关练习
一、设计思路 1、思路分析与设计 本段代码是一个使用 PyQt6 和 OpenCV 创建的图像处理应用程序。其主要功能是通过一个图形界面让用户对图片进行基本的图像处理操作,如灰度化、翻转、旋转、亮度与对比度调整,以及一些滤镜效果(模糊、锐化、边…...
石岩路边理发好去处
周末带娃去罗租公园玩,罗租公园旁边就是百佳华和如意豪庭小区,发现如意豪庭小区对面挺多路边理发摊点 理发摊点聚焦在这里的原因是刚好前面城管来了暂时避避,例如还有一个阿姨剪到一半就跟着过来。这里的城管只是拍了一处没有摊位的地方&…...
音视频入门基础:MPEG2-PS专题(2)——使用FFmpeg命令生成ps文件
一、错误的命令 通过FFmpeg命令可以将mp4文件转换为ps文件,PS文件中包含PS流数据。 由于PS流/PS文件对应的FFInputFormat结构为: const FFInputFormat ff_mpegps_demuxer {.p.name "mpeg",.p.long_name NULL_IF_CONFIG_SMALL…...
整合版canal ha搭建--基于1.1.4版本
开启MySql Binlog(1)修改MySql配置文件(2)重启MySql服务,查看配置是否生效(3)配置起效果后,创建canal用户,并赋予权限安装canal-admin(1)解压 canal.admin-1…...
[python SQLAlchemy数据库操作入门]-15.联合查询,跨表获取股票数据
哈喽,大家好,我是木头左! 在开始探讨如何利用SQLAlchemy实现复杂的联合查询之前,首先需要深入理解其核心组件——对象关系映射(ORM)。ORM允许开发者使用Python类来表示数据库中的表,从而以一种更直观、面向对象的方式来操作数据库。 SQLAlchemy中的JOIN操作详解 在SQLA…...
PTA数据结构作业一
6-1 链表的插入算法 本题要求实现一个插入函数,实现在链表llist中的元素x之后插入一个元素y的操作。 函数接口定义: int InsertPost_link(LinkList llist, DataType x, DataType y); 其中 llist是操作的链表,x是待插入元素y的前驱节点元素…...
前端(九)js介绍(2)
js介绍(2) 文章目录 js介绍(2)一、函数1.1函数的两种形式1.2函数的作用域1.3声明与提升 二、bom操作三、dom操作 一、函数 1.1函数的两种形式 //有参函数 //js中的函数只能返回一个值,如果要返回多个需要放在数组或对象中 function func(a,b){return ab } func(1,…...
CUTLASS:高性能 CUDA 线性代数模板库详解
CUTLASS:高性能 CUDA 线性代数模板库详解 引言什么是 CUTLASS?CUTLASS 的主要特点: CUTLASS 的用途如何安装 CUTLASS1. 环境准备2. 下载 CUTLASS3. 构建 CUTLASS4. 设置环境变量5. 验证安装 使用 CUTLASSCUTLASS 的优势总结 引言 在深度学习…...
关于CISP报名费用详情
CISP即“注册信息安全专业人员”,是中国信息安全测评中心实施的国家认证项目,旨在培养信息安全领域的专业人才。对于有意报考CISP的考生而言,了解报名考试费用是备考过程中不可或缺的一环。 CISP的报名考试费用主要包括培训费用、考试费用、…...
css 关于flex布局中子元素的属性flex
css flex布局中子元素的属性flex 1. flex 是 flex-grow、flex-shrink 和 flex-basis 的简写 语法格式: flex: [flex-grow] [flex-shrink] [flex-basis];各属性解析: flex-grow: 子元素如何按比例分配父元素的 剩余空间。 默认值:0&#…...