Kotlin协程简述与上下文和调度器(Dispatchers )

协程概述

子程序或者称为函数,在所有的语言中都是层级调用,如:A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。所以子程序是 通过栈来实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。

而协程看上去是子程序,执行的过程中,在 子程序中可中断,去执行其他的子程序,在适当的时候可以回来接着执行。

Kotlin协程工作原理

Kotlin 协程的大致的执行流程如上图所示,这个流程是各种类型的协程执行时都大致遵循的流程,不是一个严格精确的执行流程。

创建并启动协程

fun create.main() {
    //1. 创建协程体
    val coroutine = suspend {
        println("in coroutine")
        5
    }.createCoroutine(object: Continuation<Int> {
        override fun resumeWith(result: Result<Int>) {
            println("coroutine end: $result")
        }
​
        override val context: CoroutineContext
            get() = EmptyCoroutineContext
​
    })
​
    //2. 执行协程
    coroutine.resume(Unit)
}

上面代码的输出结果:

in coroutine
coroutine end: Success(5)

协程的执行过程

调用栈流程如下

  1. 我们通过 suspend block#createCoroutine 得到的 coroutine 实际是 SafeContinuation 对象
  2. SafeContinuation 实际上是代理类,其中的 delegate 属性才是真正的 Continuation 对象
  3. suspend block 中的代码在 BaseContinuationImpl 中执行
  4. 我们的匿名内部类对象 Continuation 被回调

suspend block 是如何变为协程体被执行的?

我们分析调用栈得知,resumeWith 最终是在 BaseContinuationImpl 中执行的,下面来看看代码

@SinceKotlin("1.3")
internal abstract class BaseContinuationImpl(
    public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
    public final override fun resumeWith(result: Result<Any?>) {
        var current = this
        var param = result
        while (true) {
            probeCoroutineResumed(current)
            with(current) {
                val completion = completion!!
                val outcome: Result<Any?> =
                    try {
                        val outcome = invokeSuspend(param) //1.这里执行了 suspend block
                        if (outcome === COROUTINE_SUSPENDED) return
                        Result.success(outcome)
                    } catch (exception: Throwable) {
                        Result.failure(exception)
                    }
                releaseIntercepted()
                if (completion is BaseContinuationImpl) {
                    current = completion
                    param = outcome
                } else {
                    completion.resumeWith(outcome) //2.这里回调了我们的匿名内部类
                    return
                }
            }
        }
    }
​
    protected abstract fun invokeSuspend(result: Result<Any?>): Any? //3. 抽象方法
}

在代码注释 1. 处,调用 current.invokeSuspend,执行了我们定义的协程体,证明 suspend block 其实是 BaseContinuationImpl 的子类

在 2. 处,协程体执行完毕后,我们的代码收到了完成回调

在 3. 处,可以发现 invokeSuspend 是个抽象方法,suspend block 就是这个方法的具体实现

下面我通过断点,进一步分析 suspend block 是通过哪个子类执行的。

可以看到 current 是名为 {文件} 方法 {方法} 方法{变量}$1 格式的对象,证明 kotlin 编译器遇到 suspend 关键字后会帮我们生成一个 BaseContinuationImpl 的子类

那么,这个子类到底是什么呢?将 kt 编译为 .class 再通过 jadx 打开后,得到的 java 代码如下

public final class CreateCoroutineKt {
    public static final void create.main() {
        Continuation coroutine = ContinuationKt.createCoroutine(new CreateCoroutineKt$create.main$coroutine$1(null), new CreateCoroutineKt$create.main$coroutine$2());
        Unit unit = Unit.INSTANCE;
        Result.Companion companion = Result.Companion;
        coroutine.resumeWith(Result.constructor-impl(unit));
    }
}
final class CreateCoroutineKt$create.main$coroutine$1 extends SuspendLambda implements Function1<Continuation<? super Integer>, Object> {
    int label;
​
    CreateCoroutineKt$create.main$coroutine$1(Continuation<? super CreateCoroutineKt$create.main$coroutine$1> continuation) {
        super(1, continuation);
    }
​
    @NotNull
    public final Continuation<Unit> create(@NotNull Continuation<?> continuation) {
        return new CreateCoroutineKt$create.main$coroutine$1(continuation);
    }
​
    @Nullable
    public final Object invoke(@Nullable Continuation<? super Integer> continuation) {
        return create(continuation).invokeSuspend(Unit.INSTANCE);
    }
​
    @Nullable
    public final Object invokeSuspend(@NotNull Object obj) {
        IntrinsicsKt.getCOROUTINE_SUSPENDED();
        switch (this.label) {
            case 0:
                ResultKt.throwOnFailure(obj);
                System.out.println((Object) "in coroutine"); //协程体的逻辑
                return Boxing.boxInt(5);
            default:
                throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
        }
    }
}

明显看出,kt 编译器帮助我们把 suspend 关键字变为了 SuspendLambda 的 子类,并重写了 invokeSuspend 方法,不难猜出 SuspendLambda 继承自 BaseContinuationImp。

Kotlin全解析文档:《Kotlin手册》点击可以查看详细类目

协程上下文与调度器

协程的上下文通常是CoroutineContext类型为代表。这个类型是被定义在Kotlin的标准库中。

在协程中,上下文是各种不同元素的集合。而其中主导作用的元素就是Job。

我们在了解协程的并发与调度的时候涉及到了Job。Kotlin 协程 组合挂起函数和async关键字,实现协程的并发操作 (zinyan.com)

这篇继续深入了解Job。

调度器(Dispatchers )与线程

什么是调度器?调度器就是一个决定了协程在哪个线程或者哪些线程上执行的控制对象。

它可以将协程限制在一个特定的线程执行,也可以把协程分配到一个线程池,或者让协程不受限制约束的进行运行。

协程上下文对象:CoroutineContext。

协程调度器对象:CoroutineDispatcher。

而我们通常在使用launch 或者async时可以通过可选参数定义CoroutineContext 对象。然后它会帮我们指定一个调度器对象。也可以使用Dispatchers 对象,定义调度器

示例:

import kotlinx.coroutines.*
​
fun main() = runBlocking<Unit> {
    // 运行在父协程的上下文中,即 runBlocking 主协程
    launch {
        println("main runBlocking      : 我工作的线程 ${Thread.currentThread().name}")
    }
    //调度到主线层中,并且不受限制
    launch(Dispatchers.Unconfined) { // 
        println("Unconfined            : 我工作的线程 ${Thread.currentThread().name}")
        
            println("这个节点是什么事实结束呢")
    }
    //调度到默认线程
    launch(Dispatchers.Default) { 
        println("Default               : 我工作的线程 ${Thread.currentThread().name}")
    }
    // 调度到一个新的线程之中
    launch(newSingleThreadContext("ZinyanThread")) { 
        println("ZinyanThreadContext   : 我工作的线程 ${Thread.currentThread().name}")
    }
}
//输出
Unconfined            : 我工作的线程 main
Default               : 我工作的线程 DefaultDispatcher-worker-1
ZinyanThreadContext   : 我工作的线程 ZinyanThread
main runBlocking      : 我工作的线程 main

下面介绍上面的四种调度逻辑。

launch{…}:默认情况下,它将会从启动它的协程对象中继承上下文以及调度器。

我们上面的例子就是,从main线程中的runBlocking协程对象中继承了上下文,结果显示运行在了main线程之中。

Dispatchers.Unconfined:是特殊的调度器,上面的例子中是运行在了main线层。但是有一个注释,叫做非受限的调度器。然后可以看到,它的输出是最快最早的。但它仅仅只是运行到第一个挂起点。挂起后,它恢复线程中的协程,而这完全由被调用的挂起函数来决定。非受限的调度器非常适用于执行不消耗 CPU 时间的任务,以及不更新局限于特定线程的任何共享数据(如UI)的协程。

它会默认继承外部协程对象。当它被限制在了调用者线程时,继承自它将会有效地限制协程在该线程运行并且具有可预测的 FIFO 调度。

例子:

fun main() = runBlocking<Unit> {
    // 非受限的——将和主线程一起工作
    launch(Dispatchers.Unconfined) {
        println("Unconfined      : 工作线程 ${Thread.currentThread().name}")
        delay(500)
        println("Unconfined      : 线程延迟 ${Thread.currentThread().name}")
    }
    launch { // 父协程的上下文,主 runBlocking 协程
        println("main runBlocking: 工作线程 ${Thread.currentThread().name}")
        delay(1000)
        println("main runBlocking: 线程延迟 ${Thread.currentThread().name}")
    }
}
//输出
Unconfined      : 工作线程 main
main runBlocking: 工作线程 main
Unconfined      : 线程延迟 kotlinx.coroutines.DefaultExecutor
main runBlocking: 线程延迟 main

所以,该协程的上下文继承自 runBlocking {…} 协程并在 main 线程中运行,当 delay 函数调用的时候,非受限的那个协程在默认的执行者线程中恢复执行。

非受限的调度器是一种高级机制,可以在某些极端情况下提供帮助而不需要调度协程以便稍后执行或产生不希望的副作用, 因为某些操作必须立即在协程中执行。非受限调度器不应该在通常的代码中使用。

Dispatchers.Default:默认调度器。默认调度器使用共享的后台线程池。所以 launch(Dispatchers.Default) { …… } 与 GlobalScope.launch { …… } 使用相同的调度器。

newSingleThreadContext(“MyOwnThread”) :自定义协程线层。为协程的运行启动了一个线程。一个专用的线程是一种非常昂贵的资源。在实际开发中两者都必须被释放,当不再需要的时候,使用 close 函数,或存储在一个顶层变量中使它在整个应用程序中被重用。否则就会出现线程泛滥的情况。

不同线程中的跳转

实现两个协程线程的跳转。示例:

fun main() = runBlocking<Unit> {
    newSingleThreadContext("Ctx1").use { ctx1 ->
        newSingleThreadContext("Ctx2").use { ctx2 ->
            runBlocking(ctx1) {
                println("开启 Ctx1")
                withContext(ctx2) {
                    println("Ctx2 开始工作")
                }
                println("返回到 Ctx1 ")
            }
        }
    }
}
//输出
开启 Ctx1
Ctx2 开始工作
返回到 Ctx1

在这个示例中, 使用runBlocking 显式指定了一个上下文。并且之后在协程中使用withContext来改变协程的上下文,而仍然驻留在相同的协程中。

得到上面的输出结果。在这个例子中,使用的都是newSingleThreadContext()创建的线程,而我们使用了标准库中的use函数来释放该线程。避免线程的滥用。

上下文中的Job

协程中的Job是上下文的一部分,并可以使用coroutineContext [Job] 表达式在上下文中检索它。

示例:

fun main() = runBlocking<Unit> {
    println("My job is ${coroutineContext[Job]}")
}
//输出
My job is BlockingCoroutine{Active}@1de0aca6

那么这个有什么作用呢?例如我们可以查询协程的活动状态

示例:

fun main() = runBlocking<Unit> {
    println("My job is ${coroutineContext[Job]}")
    var s = coroutineContext[Job]?.isActive
    println(s)
}
//输出
true

说明我当前的协程对象是活动的。

而为什么要添加“?” 那是因为对象可能为null。

子协程

当一个协程被其他协程在CoroutineScope中被启动的话,它将会通过CoroutineScope.coroutineContext来继承主协程的上下文。并且这个新协程的Job对象将会成为父协程的子Job对象。

当一个父协程被取消的时候,所有它的子协程也会被递归的取消。

但是,当使用 GlobalScope 来启动一个协程时,则新协程的作业没有父作业。因此它与这个启动的作用域无关且独立运作。

示例:

fun main() = runBlocking<Unit> {
    // 启动一个协程来处理某种传入请求(request)
    val request = launch {
        // 孵化了两个子作业, 其中一个通过 GlobalScope 启动
        GlobalScope.launch {
            println("job1: 我运行在GlobalScope启动的协程中")
            delay(1000)
            println("job1: 等待了1秒,你会发现我不受取消方法的影响")
        }
        // 另一个则承袭了父协程的上下文
        launch {
            delay(100)
            println("job2: 我是一个父协程启动的子协程对象")
            delay(1000)
            println("job2: 等待1秒,如果父协程被取消后,我也将会被取消。这行就不应该打印")
        }
    }
    delay(500)
    request.cancel() // 取消请求(request)的执行
    delay(1000) // 延迟一秒钟来看看发生了什么
    println("main: 整个协程全部取消后")
}
//输出
job1: 我运行在GlobalScope启动的协程中
job2: 我是一个父协程启动的子协程对象
job1: 等待了1秒,你会发现我不受取消方法的影响
main: 整个协程全部取消后

我们通过输出结果就可以看看到。只有job1 的两个方法被执行了。而job2 在取消过程中也被跟着进行了取消。

父协程

我们了解了子协程的概念后,才能比较清晰的明白父协程。

一个父协程总是等待所有的子协程执行结束。父协程并不显式的跟踪所有子协程的启动,并且不必使用 Job.join在最后的时候等待它们:

示例:

fun main() = runBlocking<Unit> {
    // 启动一个协程来处理某种传入请求(request)
    val request = launch {
        repeat(3) { i -> // 启动少量的子协程
            launch  {
                delay((i + 1) * 200L) // 延迟 200 毫秒、400 毫秒、600 毫秒的时间
                println("协程:$i 结束")
            }
        }
        println("返回值:父协程本身已经执行完毕了,但我并没有调用方法明确的关闭所有子协程, 子协程的事务还没有结束")
    }
    request.join() // 等待请求的完成,包括其所有子协程
    println("所有的协程结束")
}
//输出
返回值:父协程本身已经执行完毕了,但我并没有调用方法明确的关闭所有子协程, 子协程的事务还没有结束
协程:0 结束
协程:1 结束
协程:2 结束
所有的协程结束

我们可以看到,父协程的代码已经执行完毕并输出了。但是子协程仍然处于活动状态,那么整个协程就仍然属于活动状态。

当然,我们如果主动调用.cancel() 那么子协程还没有运行完也会被强制结束了。

这就是协程中的父子协程之间的关系了。

给协程命名-方便进行调试

协程如果打印日志的时候,是会有默认Id的。但是如果是在处理一些特定的请求或者逻辑的话

我们给协程进行命名,那我们在调试的时候就能更方便的进行调试了。

给协程命名通常是通过CoroutineName进行处理。

示例:

val v1 = async(CoroutineName("v1coroutine")) {
    delay(500)
    log("Computing v1")
    252
}
val v2 = async(CoroutineName("v2coroutine")) {
    delay(1000)
    log("Computing v2")
    6
}

例如上面就是我将两个协程对象 进行了命名。这种命名结果只有在log日志中才能看到结果。

初始协程时,多元素添加

我们学过载协程中初始化调度器,在上一步也学习了添加协程名称。

那么我们如果在启动的时候这两个配置属性都要进行添加,那么该如何处理?

可以通过+进行拼接。示例:

fun main() = runBlocking<Unit> {
    // 启动一个协程来处理某种传入请求(request)
    launch(Dispatchers.Default + CoroutineName("test")) {
        println("我工作的线程:${Thread.currentThread().name}")
    }
}

作用域-CoroutineScope

作用域我们都理解,就是在指定空间和区域内生效而已。而我们如果在Android开发中,使用Activity启动一个协程来处理网络或者异步IO读取等操作。所有的这个协程应该在Activity被销毁后自动取消,来避免内存泄露。

我们除了可以手动处理,并关闭外,我们还可以在协程构建的时候进行声明它的范围。

示例:

class DemoActivity : AppCompatActivity() {
    //MainScope 是使用 Kotlinx协程库自带的工厂函数。
    // 它是使用Dispatchers.Main作为调度器的适配UI线程
    private val mainScope = MainScope()
​
    //关闭的时候 取消作用域
    fun destroy() {
        mainScope.cancel()
    }
​
    fun doSomething() {
        // 在示例中启动了 10 个协程,且每个都工作了不同的时长
        repeat(10) { i ->
            mainScope.launch {
                delay((i + 1) * 200L) // 延迟 200 毫秒、400 毫秒、600 毫秒等等不同的时间
                println("Coroutine $i is done")
            }
        }
    }
}

然后我们如果关闭activity,协程也会自动进行关闭。

Android 现在在所有具有生命周期的实体中(activity,Fragment等),都对协程作用域提供了一级支持。

局部数据传递

我们如果使用协程,特别是子协程,父协程混杂等等情况。那么如果能够将一些数据在协程与协程之间传递。那么将会大大提高效率。

Kotlin 提供了:ThreadLocal,asContextElement 扩展函数来帮助我们,它们创建了额外的上下文元素, 且保留给定 ThreadLocal 的值,并在每次协程切换其上下文时恢复它。

示例:

import kotlinx.coroutines.*
​
val threadLocal = ThreadLocal<String?>() // declare thread-local variable
​
fun main() = runBlocking<Unit> {
    threadLocal.set("main")
    println("Pre-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
    val job = launch(Dispatchers.Default + threadLocal.asContextElement(value = "launch")) {
        println("Launch start, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
        yield()
        println("After yield, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
    }
    job.join()
    println("Post-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
}
//输出
Pre-main, current thread: Thread[main,5,main], thread local value: 'main'
Launch start, current thread: Thread[DefaultDispatcher-worker-1,5,main], thread local value: 'launch'
After yield, current thread: Thread[DefaultDispatcher-worker-1,5,main], thread local value: 'launch'
Post-main, current thread: Thread[main,5,main], thread local value: 'main'

在这个示例中,使用Dispatchers.Default 在线程池中启动了一个新的协程。所以它工作在线程池中的不同线程中,但它仍然具有线程局部变量的值,例如上面就是使用asContextElement 修改get的值从main 改为launch。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用安全复制命令scp在Windows系统和Linux系统之间相互传输文件

现在已经有很多远程控制服务器的第三方软件平台&#xff0c;比如FinalShell&#xff0c;MobaXterm等&#xff0c;半可视化界面&#xff0c;使用起来非常方便和友好&#xff0c;两个系统之间传输文件直接拖就行&#xff0c;当然也可以使用命令方式在两个系统之间相互传递。 目录…

git 基础

1.下载安装Git&#xff08;略&#xff09; 2.打开git bash窗口 3.查看版本号、设置用户名和邮箱 用户名和邮箱可以随意起&#xff0c;与GitHub的账号邮箱没有关系 4.初始化git 在D盘中新建gitspace文件夹&#xff0c;并在该目录下打开git bash窗口 git init 初始化完成后会…

基于深度学习的机器视觉表计识别

01 引言 针对仪表自动读数问题&#xff0c;新型数字式仪表的读数比较方便&#xff0c;现阶段已经有非常多成熟的方案落地&#xff0c;而针对传统指针式仪表自动读数的现有方案还不够成熟&#xff0c;存在识别不精确、易受环境干扰等问题&#xff0c;是亟待研究和攻克的难题。我…

ICS PA1

ICS PA1 init.shmake 编译加速ISA计算机是个状态机程序是个状态机准备第一个客户程序parse_argsinit_randinit_loginit_meminit_isa load_img剩余的初始化工作运行第一个客户程序调试&#xff1a;零断点TUI 基础设施单步执行打印寄存器状态扫描内存 表达式求值词法分析递归求值…

Vue.js2+Cesium1.103.0 十一、Three.js 炸裂效果

Vue.js2Cesium1.103.0 十一、Three.js 炸裂效果 Demo ThreeModelBoom.vue <template><div:id"id"class"three_container"/> </template><script> /* eslint-disable eqeqeq */ /* eslint-disable no-unused-vars */ /* eslint-d…

20 MySQL(下)

文章目录 视图视图是什么定义视图查看视图删除视图视图的作用 事务事务的使用 索引查询索引创建索引删除索引聚集索引和非聚集索引影响 账户管理&#xff08;了解非DBA&#xff09;授予权限 与 账户的相关操作 MySQL的主从配置 视图 视图是什么 通俗的讲&#xff0c;视图就是…

(Windows )本地连接远程服务器(Linux),免密码登录设置

在使用VScode连接远程服务器时&#xff0c;每次打开都要输入密码&#xff0c;以及使用ssh登录或其它方法登录&#xff0c;都要本地输入密码&#xff0c;这大大降低了使用感受&#xff0c;下面总结了免密码登录的方法&#xff0c;用起来巴适得很&#xff0c;起飞。 目录 PowerSh…

数据仓库总结

1.为什么要做数仓建模 数据仓库建模的目标是通过建模的方法更好的组织、存储数据&#xff0c;以便在性能、成本、效率和数据质量之间找到最佳平衡点。 当有了适合业务和基础数据存储环境的模型&#xff08;良好的数据模型&#xff09;&#xff0c;那么大数据就能获得以下好处&…

深入了解Nginx:高性能的开源Web服务器与反向代理

一、Nginx是什么 Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件&#xff08;IMAP/POP3&#xff09;代理服务器&#xff0c;也可以作为负载均衡器和HTTP缓存服务器使用。它采用事件驱动、异步非阻塞的处理方式&#xff0c;能够处理大量并发连接和高流量负载&#xff…

supervisorctl(-jar)启动配置设置NACOS不同命名空间

背景 由于需要在上海服务器上面配置B测试环境&#xff0c;原本上面已有A测试环境&#xff0c;固需要将两套权限系统分开 可以使用不同的命名空间来隔离启动服务 注&#xff1a;本文章均不涉及公司机密 1、新建命名空间 命名空间默认会有一个public&#xff0c;并且不能删除&a…

Java 中数据结构HashMap的用法

Java HashMap HashMap 是一个散列表&#xff0c;它存储的内容是键值对(key-value)映射。 HashMap 实现了 Map 接口&#xff0c;根据键的 HashCode 值存储数据&#xff0c;具有很快的访问速度&#xff0c;最多允许一条记录的键为 null&#xff0c;不支持线程同步。 HashMap 是…

自然语言处理2-NLP

目录 自然语言处理2-NLP 如何把词转换为向量 如何让向量具有语义信息 在CBOW中 在Skip-gram中 skip-gram比CBOW效果更好 CBOW和Skip-gram的算法实现 Skip-gram的理想实现 Skip-gram的实际实现 自然语言处理2-NLP 在自然语言处理任务中&#xff0c;词向量&#xff08;…

分布式集群框架——有关zookeeper的面试考点

3.掌握Zookeeper的概念 当涉及到大规模分布式系统的协调和管理时&#xff0c;Zookeeper是一个非常重要的工具。 1. 分布式协调服务&#xff1a;Zookeeper是一个分布式协调服务&#xff0c;它提供了一个高可用和高性能的环境&#xff0c;用于协调和同步分布式系统中的各个节点…

js深拷贝三种方法

使用递归函数实现深拷贝 const obj {name: zzz,age: 18,hobby: [篮球, 足球],family: {baby: baby}} // 深拷贝 数组 对象 一定要先筛数组再筛对象,因为万物皆对象function deepcopy(newObj, oldObj) {for (const k in oldObj) {// 判断值是否属于array类if (oldObj[k] i…

[Android AIDL] --- AIDL工程搭建

0 AIDL概念 AIDL&#xff08;Android Interface Definition Language&#xff09;是一种 IDL 语言&#xff0c;用于生成可以在 Android 设备上两个进程之间进行进程间通信&#xff08;IPC&#xff09;的代码。 通过 AIDL&#xff0c;可以在一个进程中获取另一个进程的数据和调…

FPGA VR摄像机-拍摄和拼接立体 360 度视频

本文介绍的是 FPGA VR 相机的第二个版本&#xff0c;第一个版本是下面这样&#xff1a; 第一版地址&#xff1a; ❝ https://hackaday.io/project/26974-vr-camera-fpga-stereoscopic-3d-360-camera ❞ 本文主要介绍第二版本&#xff0c;第二版本的 VR 摄像机&#xff0c;能够以…

从零做软件开发项目系列之八——系统部署调试

前言 软件项目的部署和调试工作是项目开发生命周期中的重要阶段&#xff0c;它涉及将开发完成的软件应用程序部署到目标环境并进行测试和调试&#xff0c;以确保系统能够正常运行并满足用户需求。本文将详细描述软件项目的部署和调试工作。 1 硬件基础设施和操作系统及基本软…

Python科研绘图--Task05

目录 SciencePlots 安装SciencePlots 安装LaTeX ① 安装 MikTex 和 Ghostscript ② 将软件的安装路径添加到系统环境变量中 SciencePlots 绘图示例 SciencePlots 虽然 Matplotlib 或 ProPlot 库能够绘制出插图结果&#xff0c;但用户还需要根据期刊的配图绘制要求进行…

Flink CDC介绍

1.CDC概述 CDC&#xff08;Change Data Capture&#xff09;是一种用于捕获和处理数据源中的变化的技术。它允许实时地监视数据库或数据流中发生的数据变动&#xff0c;并将这些变动抽取出来&#xff0c;以便进行进一步的处理和分析。 传统上&#xff0c;数据源的变化通常通过…

PCL common模块应用实例

目录 一、common模块中的头文件二、基本函数1、angles.h2、centriod.h3、common.h4、distance.h5、copy_point.h6、geometry.h参考链接本文由CSDN点云侠原创,