C++20 协程(coroutine)入门

news2024/12/26 15:01:23

文章目录

  • C++20 协程(coroutine)入门
    • 什么是协程
      • 无栈协程和有栈协程
      • 有栈协程的例子
        • 例 1
        • 例 2
      • 对称协程与非对称协程
      • 无栈协程的模型
      • 无栈协程的调度器
        • 朴素的单线程调度器
        • 让协程学会等待
        • Python 中的异步函数
        • 可等待对象
        • M:N 调度器——C# 中的异步函数
      • 小结
    • C++20 中的协程对象
      • (未完待续)

在阅读下面的内容之前,建议先入门至少三门除 C++ 外的其他编程语言,最好还支持协程。

可以参考:渡劫 C++ 协程(0):前言 | Benny Huo

C++20 协程(coroutine)入门

什么是协程

可以参考:初识协程 | 楚权的世界 (chuquan.me)

老生常谈,协程的核心思想是允许放弃执行当前函数,转而执行其他函数,但之后还能恢复之前函数的执行状态。学过 Python 的人很快就能想到,这不就是生成器吗?

程序 1(Python):生成器
def my_range(to): # 是一个生成器。
    for i in range(1, to + 1):
        yield i # 1. 放弃执行当前函数。

if __name__ == "__main__":
    for i in my_range(3): # 3. 恢复之前函数的执行状态。
        print(i) # 2. 转而执行其他函数。

但这个和什么所谓的“亿级别流量洪峰”有什么关系,怎么做到让数亿协程“宏观并行”(即表现出并发特征)?如果没有新的线程被创建,网络调用仍然只能在主线程执行,这个矛盾怎么解决?我相信即使你不会 Python,看不懂上面的代码,在看别人对协程的介绍时也能想到这些问题。

我们一步一步来,先巩固协程相关的基本概念,再来回答以上刁钻的问题。

无栈协程和有栈协程

可以参考:浅谈有栈协程与无栈协程 - 知乎 (zhihu.com)。

可以参考:协程和纤程的区别是什么? - tearshark的回答 - 知乎。

可以参考:有栈协程与无栈协程 (mthli.xyz)

协程(coroutine),也就是协作(co-)的过程(routine),离不开过程二字,也就是说协程也是一个函数(function, method, routine, etc.)。同时可以顾名思义,互相“协作”的“过程”生来就是用于解决并发问题的。

我们都知道,一般的线程一定存在一个函数调用栈,记录着函数之间的调用关系、局部变量、返回地址等等。那对于协程来说,它和我们熟知的那个栈有什么关系呢?

有什么关系,其实取决于“协作”的具体实现。不同的实现会与我们熟知的那个栈产生不同的联系。大体上可以分为两类:

  1. 有栈协程(stackful)。

    创建一个有栈协程时,运行时(runtime)会申请一片内存空间,作为协程的栈空间。之后,该协程都将这片空间视为自己的栈空间。如果已经开始执行该协程的代码,这个协程就好像在一个新的线程上运行一样。

    但创建一个有栈协程并不会创建一个内核态的线程,如何使得协程具有并发特征?其实关键还是在于让协程自己放弃当前的执行权。

  2. 无栈协程(stackless)。

    创建一个无栈协程时,运行时会申请一片内存空间,保存协程的栈帧。之后,该协程仍然在某个线程的栈空间上运行,只不过协程可以选择保存当前栈帧后放弃执行权,再之后还可以恢复到此前的状态继续执行。

简单地说,这两类协程可以描述为(不一定准确,主要是为了方便理解):

  1. 有栈协程就是不由操作系统内核调度的“线程”。取决于具体实现,可能没有线程本地存储(Thread Local Storage, TLS),等等,总而言之只是长得像线程。
  2. 无栈协程就是一个可以断断续续执行的函数。

Python 的生成器可以看作是无栈协程,C++20 提供的协程也是无栈协程。

有栈协程的例子

介绍以上分类,其实对理解协程提供并发能力并没有任何帮助。一方面,一开始提到的 Python 的生成器也是无栈协程,但我们(可能)并没有见过用生成器解决并发问题的场景,所以之前的提问一个也没有被解答。

为了更直观地看到协程如何解决并发问题,我们来看几个有栈协程的例子。

例 1

程序 2(C 语言):Windows 中的纤程(fiber),是有栈协程的一种实现,在单线程中实现并发
#include <stdbool.h>
#include <stdio.h>

#include <Windows.h>

PVOID fiber_main;
PVOID fiber_anothers[2];

void inner(int id) {
	printf("Task %d\n", id);
	// Note:放弃当前纤程执行权,转换到其他纤程。
	SwitchToFiber(fiber_main);
}

void WINAPI another(LPVOID param) {
	while (true) {
		inner((int)param);
	}
}

int main() {
	// 将当前的线程转换为纤程,允许参与纤程的调度。
	fiber_main = ConvertThreadToFiber(NULL);
	// 创建纤程,但不执行。
	for (unsigned i = 0; i < 2; i++) {
		// 参数 1 是栈空间,0 表示取默认值。
		fiber_anothers[i] = CreateFiber(0, another, (LPVOID)(i + 1));
	}

	printf("Fiber demo started\n");
	for (unsigned i = 0; i < 3; i++) {
		for (unsigned j = 0; j < 2; j++) {
			// Note:放弃当前纤程执行权,转换到其他纤程。
			SwitchToFiber(fiber_anothers[j]);
		}
	}
	printf("Done!\n");

	// 回收资源。
	for (unsigned i = 0; i < 2; i++) {
		// 即使两个任务是死循环,也因为放弃执行权而没有运行。
		// 由于纤程是我们自己调度,所以可以安全地删除它们。
		DeleteFiber(fiber_anothers[i]);
	}
	ConvertFiberToThread();
}

运行结果:

Fiber demo started
Task 1
Task 2
Task 1
Task 2
Task 1
Task 2
Done!

程序 1 的 another 函数是一个典型的协程。它运行时可以表现出并发的特征,前提是我们需要自己放弃当前协程的执行权(SwitchToFiber 函数)。即使是在协程调用的子函数中(inner 函数),也可以主动放弃当前协程的执行权,所以 Windows 中的纤程是有栈协程的一种实现。

例 2

程序 3(Go 语言):goroutine 是有栈协程的一种实现,通过运行时调度器实现并发
package main

import (
	"fmt"
	"time"
)

func inner(id int) {
	fmt.Println("Task", id)
	// Note: 放弃当前 goroutine 执行权,转换到其他 goroutine。
	time.Sleep(100 * time.Millisecond)
	// Note: 运行时会帮助我们尽可能在 100 毫秒后重新取得执行权。
}

func another(id int) {
	for true {
		inner(id)
	}
}

func main() {
	fmt.Println("goroutine demo started")
	for i := 0; i < 2; i++ {
		// 创建 goroutine,是否立即开始在其他线程中执行取决于运行时。
		go another(i)
	}
	// Note: 放弃当前 goroutine 执行权,转换到其他 goroutine。
	time.Sleep(300 * time.Millisecond)
	fmt.Println("Done!")
	// Note: 主 goroutine 被销毁后,其他 goroutine 也被销毁。
}

可能的运行结果:

goroutine demo started
Task 0
Task 1
Task 0
Task 1
Task 1
Task 0
Done!

通过这两个例子,我们大致看到了有栈协程在实现并发时不可或缺的东西:调度(schedule)。例 1 中,调度完全由手工实现(SwitchToFiber 函数),费时费力;而例 2 中,调度由 Go 语言的**调度器(scheduler)**实现,写程序时只用自然地让当前 goroutine 睡眠即可(time.Sleep 函数)。

为了实现 M:N 模型,Go 语言运行时提供的调度器颇为复杂,但使用 Go 语言时就不用考虑这么多了,就程序 3 而言,把 goroutine 看作一个线程也无妨。Go 语言的调度器让有栈协程具有了很多类似线程的功能,从而可以像线程一样使用 goroutine,同时让创建 goroutine 的代价很低,也就实现了高并发。

从 Go 语言可以看出,如果一个语言支持有栈协程,那么把原有的线程函数迁移为协程函数并不会太复杂,因为它们长得挺像。但对于无栈协程来说,没有长得像一说,所以代码迁移可能会花更多时间。但无栈协程所占空间明显小于有栈协程,这是无栈协程特有的优势。

对称协程与非对称协程

可以参考:协程学习(对称和非对称) - 知乎 (zhihu.com)

可以参考:一文彻底弄懂C++开源协程库libco——原理及应用 - 知乎 (zhihu.com)

程序 3 中,goroutine 通过 go 语句被创建后,就好像一个单独的线程一样,被创建的协程只能自己选择放弃(yield)执行权,至于放弃之后谁执行,只由调度器决定,不由协程的创建者决定,这种就是对称协程(symmetric coroutine)。对称协程之间不存在明显的从属关系,大家都是平等的。

程序 2 中,我们完全自己调度纤程。如果规定在放弃执行权时只能回到纤程的创建者,则可以形成纤程的调用关系链。这种具有明显调用关系的协程就是非对称协程(asymmetric coroutine)。

由此可以注意到,无栈与有栈、对称与非对称是两个不同的概念。Python 的生成器可以看作是非对称协程,C++20 提供的协程也是非对称协程。

应该没有无栈对称协程……

无栈协程的模型

我们终于来到与 C++20 有关的东西了:无栈非对称协程。如果它不能表现得类似于一个线程,又有什么用,该怎么用?

图 1:程序 1 的大致执行流程

图 1 中,main() 表示主流程,是一个普通的函数(不妨把 Python 的主过程看成一个函数),my_range() 是生成器,也就是一个协程。图中,黑点表示可以进入的点,普通函数只有开头一个,而无栈非对称协程则可以有任意多个,每个对应 Python 中的 yield 语句。

因此,可以把协程看作一个状态机,图 1 中,协程内的黑点就对应一个状态,协程内的箭头就对应状态的转移。需要注意的是,这个状态机还有大量隐藏的状态以局部变量的形式存在于协程中,随图中可见状态的转移而转移。

无栈协程在逻辑上总是可以用闭包的形式实现,但实际上很难写,甚至可能会写不出来。尽管如此,尝试将无栈协程和闭包相互转换,对理解无栈协程的工作原理会很有帮助。

程序 4(C++):使用闭包实现一个简单的无栈协程
#include <iostream>

auto my_range() {
	// 每一个 lambda 表达式都对应图 1 协程中的一个黑点。
	int value = 0;
	// 通过按值捕获变量,将局部变量作为状态保存在闭包中。
	return [=]() mutable {
		std::cout << ++value << std::endl;
		// 通过按引用捕获变量,模拟局部变量的状态转移。
		return [&]() {
			std::cout << ++value << std::endl;
			return [&]() {
				std::cout << ++value << std::endl;
				return [&]() -> void {
					// 没有返回值。
				};
			};
		};
	};
}

int main() {
	// 类似于 Python 中的生成器对象,状态均保存在名为 coroutine 的对象中。
	auto coroutine = my_range();
	// resume_point_* 不保存变量状态,只保存执行位置。
	const auto resume_point_1 = coroutine();
	const auto resume_point_2 = resume_point_1();
	resume_point_2();
}

运行结果:

1
2
3

程序 4 对应程序 1 和图 1,是使用 C++ 中的闭包模拟无栈协程的结果。从中可以感受到,如果编译器不支持协程相关的语法,只用闭包模拟无栈协程会有相当多的困难:

  1. 协程中的状态点越多,闭包的层数就越深。

    如果尝试将闭包作为回调函数,复杂逻辑就会导致很深的闭包,称为回调地狱(callback hell)。如果能把程序 4 转换成程序 1 那样,回掉地狱问题就解决了。

    # 更接近程序 4 模拟无栈协程的 Python 生成器。
    def my_range():
        value = 0
        # 没有回调地狱!
        yield (value := value + 1)
        yield (value := value + 1)
        yield (value := value + 1)
    

    可以参考:Java如何实现一个回调地狱(Callback Hell)? - 掘金 (juejin.cn)

    通过诉诸协程解决回调地狱,靠的是扩展处理器的日常使用方法:过去我们只想到函数调用、中断,现在还可以通过自己保存栈帧来实现协程。除了向计算机底层寻求方法,还可以向更抽象的

  2. 协程中的局部变量作为内部状态,很难正确地处理。

    比如,程序 4 中一会儿按值捕获,一会儿按引用捕获,很难弄清楚,特别是有更多零散的局部变量时。

  3. 如果有复杂的结构,例如循环结构,很难、甚至不能用闭包实现。

    比如程序 4 就没有写出程序 1 中的循环结构。

  4. 闭包无法实现协程中的数据传递。

现在,我们大致明白了使用协程实现并发的方法(关键在于存在一个调度器),也知道了无栈协程的状态机模型。但我们仍然不知道如何用无栈协程实现并发,这是因为我们不知道无栈协程应该有怎样的调度器。

无栈协程的调度器

可以参考:万字好文:从无栈协程到C++异步框架! - 腾讯云技术社区 - SegmentFault 思否

可以参考:python中的yield、yield from、async/await中的区别与联系 - 简书 (jianshu.com)

可以参考:await 运算符 - 异步等待任务完成 | Microsoft Learn

可以参考:【译】图与例解读Async/Await - 知乎 (zhihu.com)

作为入门教程,我们当然不讨论无栈协程的调度器具体该怎么写,但是我们必须至少弄清楚无栈协程的调度器长什么样,不然怎么知道如何用它实现并发,怎么发挥协程的优势?

朴素的单线程调度器

很容易想到,可以让调度器变成一个死循环,不断轮流执行尚未完成的所有协程就可以了。

程序 5(Python):最朴素的想法
def my_range(to):
    for i in range(1, to + 1):
        yield i

if __name__ == "__main__":
    coroutines = [my_range(3) for _ in range(4)]
    # 如果不是所有协程都已经结束,就继续执行。
    while not all(coroutine.gi_frame is None for coroutine in coroutines):
        # 轮流执行每个协程。
        for coroutine in coroutines:
            try:
                print(next(coroutine))
            except StopIteration:
                pass

运行结果:

1
1
1
1
2
2
2
2
3
3
3
3
图 2:最朴素的想法

虽然程序 5 似乎没啥用,但是我们得知了:

  1. 调度器一定是一个普通函数,而不是协程。因为我们讨论的是非对称协程,所以这些协程放弃执行权后会自动回到调度器上次执行的位置,对调度器而言执行协程就好比执行函数一样。

    这意味着当我们希望协程表现出并发的特征时,首先需要调用一个调度器函数。

  2. 这种最朴素的调度器并不调度协程内创建的协程。比如程序 5 中,my_range 里面创建了 range,它也是一个协程,但 main 调度器看不见也管不着它。

    这意味着要想有栈协程那样允许在任意子调用中放弃执行权会很困难。

  3. 这种最朴素的调度器没有办法处理协程之间的依赖关系。比如程序 5 中,各个 my_range 产生的结果都是无关的。

    这意味着想要使用另一个协程的运行结果会很困难。

对于后两个问题,如果像程序 5 中 my_range 使用 range 那样,让协程 my_range 自己调度另一个协程 range,并且又希望使用另一个协程的最后运行结果(因为我们通常更关心函数的返回值),代码就会变得很繁琐。请看下面的 Python 程序。

程序 6(Python):最失败的 man
def my_complex_task(id):
    for i in range(3):
        print(f"Task {id}")
        yield
    # 需要拿到这个结果。
    yield id + 1

def my_print(id):
    inner_coroutine = my_complex_task(id)
    # 繁琐:怎么拿到协程的返回值?
    last_yield = None
    for result in inner_coroutine:
        last_yield = result
        # 繁琐:我自己调度,怎么知道什么时候自己该 yield?
        yield
    # 繁琐:如果这个协程也只是返回结果,然后在 main 里才进行输出,是不是以上繁琐还要再来一次?
    print(f"Result of {id}: {last_yield}")

if __name__ == "__main__":
    coroutines = [my_print(i + 1) for i in range(2)]
    # 如果不是所有协程都已经结束,就继续执行。
    while not all(coroutine.gi_frame is None for coroutine in coroutines):
        # 轮流执行每个协程。
        for coroutine in coroutines:
            try:
                next(coroutine)
            except StopIteration:
                pass

运行结果:

Task 1
Task 2
Task 1
Task 2
Task 1
Task 2
Result of 1: 2
Result of 2: 3

程序 6 确实让 my_print 协程用到了 my_complex_task 协程的结果,并且成功表现出了并发的特征,但写出来实在是太繁琐了。如果 my_print 要用到 my_complex_task 的结果,怎么做更优美?

让协程学会等待

既然 my_print 要用到 my_complex_task 的结果,那就等 my_complex_task 结束吧。

图 3:如果协程学会等待

事实上,“学会等待”是无栈协程的基本操作,因为这样就可以实现栈式的函数调用,同时保留了并发能力。在编程语言中,等待(await)就会导致协程被挂起(suspend),直到通知恢复(assume),协程才能继续被调度。用于并发操作的无栈协程本身常被称为异步(async)函数。

Python 中的异步函数

Python 的生成器虽然是无栈协程,但实际上不会用于并发场景,原因可以见程序 6。用于并发场景的无栈协程,也就是异步函数,在 Python 中的基本使用方法如下所示。

程序 7(Python):异步函数
import asyncio

# async 关键字表示这是一个协程。
async def my_complex_task(id):
    for i in range(3):
        print(f"Task {id}")
        # 主动放弃执行权。
        await asyncio.sleep(0)
    # 需要拿到这个结果。
    return id + 1
	# 结束,通知调用方(my_print),使其恢复。

async def my_print(id):
    # 声称自己要等。等到结果后才会被继续调度。
    result = await my_complex_task(id)
    print(f"Result of {id}: {result}")

if __name__ == "__main__":
    # 直接“调用”协程将会得到一个协程对象,并没有开始执行。
    tasks = [my_print(i) for i in range(3)]
    # 创建调度器。
    loop = asyncio.new_event_loop()
    # 调用调度器函数。
    loop.run_until_complete(asyncio.wait(tasks))
    # 回收调度器。
    loop.close()

运行结果:

Task 2
Task 1
Task 0
Task 2
Task 1
Task 0
Task 2
Task 1
Task 0
Result of 2: 3
Result of 1: 2
Result of 0: 1

程序 7 和程序 6 的功能一样,在单个线程中具有并发能力。但程序 7 的编写比程序 6 简单许多,正是“等待”使得无栈协程可以在调用其他协程的同时保持并发能力。缺点是,所有被调用的协程都需要用 async 关键字修饰,称这种现象为 async 传染。

图 3 说,await 会使新的协程被加入调度器,但这一点似乎从程序 7 中看不明白。事实上,要看透这一点,必须深入协程调度器的具体实现,所以这个问题需要留到讲解 C++20 的协程库时才能解决。

可等待对象

图 4:如果协程学会抽象的等待

图 4 的意思是,协程必须等待的是另一个协程吗?只要等待的对象能够恢复(resume)调用方协程、能提供运行的结果,那就可以拿来等!这种对象就称为可等待对象(awaitable object)。

虽然可等待对象可以不是协程,但一般都是协程。图 4 中的 my_task 也有可能是协程吗?事实上是可能的,只要 main_task 在首次恢复时不被调度器指派到主线程上即可。

M:N 调度器——C# 中的异步函数

可以参考:await 运算符 - 异步等待任务完成 | Microsoft Learn

至此为止,我们只实现了并发,还没有实现并行。很容易想到,要让协程拥有并行的能力,只需要让调度器支持创建多个内核态线程就好了。

实现并行的关键是在恢复协程时为它分配到另一个线程上。我们直接看看 C# 的一个例子。

程序 8(C#):异步函数(修改自官网的例子)
public class AwaitOperator
{
    public static async Task Main()
    {
        Task<int> downloading = DownloadDocsMainPageAsync(); // 立即开始执行,直到 await。返回值是 Task。
        Console.WriteLine($"{nameof(Main)}: Launched downloading. (on {Thread.CurrentThread.ManagedThreadId})");

        int bytesLoaded = await downloading;
        Console.WriteLine($"{nameof(Main)}: Downloaded {bytesLoaded} bytes. (on {Thread.CurrentThread.ManagedThreadId})");
    }

    private static async Task<int> DownloadDocsMainPageAsync()
    {
        Console.WriteLine($"{nameof(DownloadDocsMainPageAsync)}: About to start downloading. (on {Thread.CurrentThread.ManagedThreadId})");

        var client = new HttpClient();
        byte[] content = await client.GetByteArrayAsync("https://learn.microsoft.com/en-us/");

        Console.WriteLine($"{nameof(DownloadDocsMainPageAsync)}: Finished downloading. (on {Thread.CurrentThread.ManagedThreadId})");
        return content.Length;
    }
}

可能的运行结果:

DownloadDocsMainPageAsync: About to start downloading. (on 1)
Main: Launched downloading. (on 1)
DownloadDocsMainPageAsync: Finished downloading. (on 7)
Main: Downloaded 39995 bytes. (on 7)

程序 8 告诉我们:

  1. C# 可以在后台自动运行一个调度器,并且是 M:N 调度器。
  2. 调度器的调度工作在 await 语句处发生。协程挂起后,再次恢复时在哪个线程上由调度器决定。

小结

C++20 的协程是无栈非对称协程。无栈协程可以用于生成器,也可以用于并发场景。用于并发场景的协程也被称为异步函数。并发场景下,协程的调度器不可或缺。

无栈协程可以抽象为一个状态机,也可以用闭包模拟简单的无栈协程。使无栈协程并发的关键是 await 语句,可以等待对象返回结果后再接受调度。使无栈协程并行的关键是调度器,调度器可以在协程恢复运行时指派线程。不同编程语言实现的调度器各不相同,不同场景下所需的调度器也不相同,使用前需要充分调研所用调度器的特征。

C++20 中的协程对象

前面举了这么多例子,只是为了说明协程的功能。C++20 中的协程具体是怎样的?很遗憾,C++20 根本没提供协程的调度器,一切都需要自己写,所以大家才说 C++20 的协程是为库开发者准备的。

但如果学习了 C++20 中的协程,便可以说了解了协程的底层原理,处理其他语言中的协程也就游刃有余了。

(未完待续)

on 7)


程序 8 告诉我们:

1. C# 可以在后台自动运行一个调度器,并且是 M:N 调度器。
2. 调度器的调度工作在 `await` 语句处发生。协程挂起后,再次恢复时在哪个线程上由调度器决定。

### 小结

C++20 的协程是无栈非对称协程。无栈协程可以用于生成器,也可以用于并发场景。用于并发场景的协程也被称为异步函数。并发场景下,协程的调度器不可或缺。

无栈协程可以抽象为一个状态机,也可以用闭包模拟简单的无栈协程。使无栈协程并发的关键是 await 语句,可以等待对象返回结果后再接受调度。使无栈协程并行的关键是调度器,调度器可以在协程恢复运行时指派线程。不同编程语言实现的调度器各不相同,不同场景下所需的调度器也不相同,使用前需要充分调研所用调度器的特征。

## C++20 中的协程对象

前面举了这么多例子,只是为了说明协程的功能。C++20 中的协程具体是怎样的?很遗憾,C++20 根本没提供协程的调度器,一切都需要自己写,所以大家才说 C++20 的协程是为库开发者准备的。

但如果学习了 C++20 中的协程,便可以说了解了协程的底层原理,处理其他语言中的协程也就游刃有余了。

### (未完待续)

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LabVIEW深度相机与三维定位实战(下)

‍‍&#x1f3e1;博客主页&#xff1a; virobotics的CSDN博客&#xff1a;LabVIEW深度学习、人工智能博主 &#x1f384;所属专栏&#xff1a;『LabVIEW深度学习实战』 &#x1f37b;上期文章&#xff1a;『LabVIEW深度相机与三维定位实战&#xff08;上&#xff09;』 &#…

《Python入门到精通》循环语句 while循环,for循环

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;小白零基础《Python入门到精通》 循环语句 1、语法格式1.1、while1.2、死循环1.3、简写形式 2、continue 跳过循环…

synchronized总结

目录 一、synchronized的特性 1.1 原子性 1.2 可见性 1.3 有序性 1.4 可重入性 二、synchronized的使用 2.1 修饰普通方法 2.2 修饰静态方法 2.3 修饰代码块 三、synchronized的锁机制 3.1 偏向锁 3.2 轻量级锁 3.3 重量级锁 一、synchronized的特性 1.1 原子性 原子性是指一…

【逗老师的PMP学习笔记】3、项目经理的角色和能力

个人感觉这一篇属于打鸡血篇&#xff0c;与实战关联较弱。主要各位要思考一下&#xff0c;做好一个项目经理&#xff0c;在哪些overall的领域和能力上要让自己牛逼 一、开篇一张图 总结一句话&#xff0c;项目经理更像是一直大型交响乐团的**指挥** &#xff08;除了正向的&a…

Streamlit应用中构建多页面(三):两种方案

文章目录 1 前言2 第一种方案&#xff1a;使用Session State实现多页面交互2.1 Session State简介2.2 多页面应用的基本结构2.3 实现多页面交互的代码示例2.4 Session State机制的优缺点 3 第二种方案&#xff1a;Streamlit内置多页面方案&#xff08;更为推荐&#xff09;3.1 …

DoIP学习笔记系列:(三)用CAPL脚本过“安全认证”,$27服务实现

文章目录 1. 如何调用接口通过安全认证?如何新建CAPL工程,在此不再赘述,本章主要分享一下如何在CAPL中调用DoIP接口、diag接口进行DoIP和诊断的测试。 注意:CANoe工具本身的使用没什么难的,所谓会者不难难者不会,各位小伙伴有疑问要多问,多交流,往往难事都只是一层窗户…

生鲜蔬果小程序的完整教程

随着互联网的发展&#xff0c;线上商城成为了人们购物的重要渠道。其中&#xff0c;小程序商城在近年来的发展中&#xff0c;备受关注和青睐。本文将介绍如何使用乔拓云网后台搭建生鲜果蔬配送小程序&#xff0c;并快速上线。 首先&#xff0c;登录乔拓云网后台&#xff0c;进入…

(具体解决方案)训练GAN深度学习的时候出现生成器loss一直上升但判别器loss趋于0

今天小陶在训练CGAN的时候出现了绷不住的情况&#xff0c;那就是G_loss&#xff08;生成器的loss值&#xff09;一路狂飙&#xff0c;一直上升到了6才逐渐平稳。而D_loss&#xff08;判别器的loss值&#xff09;却越来越小&#xff0c;具体的情况就看下面的图片吧。其实这在GAN…

【设计模式】——工厂模式

什么是工厂模式&#xff1f; Java的工厂模式是一种创建型设计模式&#xff0c;它提供了一种创建对象的最佳方式。在工厂模式中&#xff0c;我们在创建对象时不会对客户端暴露创建逻辑&#xff0c;而是通过使用一个共同的接口来指向新创建的对象。这种类型的设计模式属于创建型…

替换开源LDAP,西井科技用宁盾目录统一身份,为业务敏捷提供支撑

客户介绍 上海西井科技股份有限公司成立于2015年&#xff0c;是一家深耕于大物流领域的人工智能公司&#xff0c;旗下无人驾驶卡车品牌Q-Truck开创了全球全时无人驾驶新能源商用车的先河&#xff0c;迄今为止已为全球16个国家和地区&#xff0c;120余家客户打造智能化升级体验…

houdini vex中的属性类型(attribute)

https://www.sidefx.com/docs/houdini/vex/snippets.html#parameters

【LUBAN】烧录经常失败怎么办?

目录 1、问题描述2、处理办法 1、问题描述 在一些PC上烧录时&#xff0c;经常出现烧录失败的问题&#xff08;如下图所示&#xff09;&#xff0c;这是因为烧录时会重启USB口&#xff0c;而PC一些其它的程序占用了此USB口&#xff0c;导致数据传输失败。多试几次也是能成功的&…

c语言指针的运算

1、通过指针计算数组的元素&#xff08;指针相减&#xff0c;类型需要一致&#xff09;&#xff0c;比如数组元素指针相减得到的是中间相差的元素个数&#xff0c;可以用于计算数组元素的个数等 #include "stdio.h" #include <stdlib.h>int main() {int a[10]…

内卷化时代,一名普通测试员的铁饭碗究竟是什么?

内卷&#xff0c;是现在热度非常高的一个词汇&#xff0c;随着热度不断攀升&#xff0c;隐隐有了“万物皆可卷”的程度。究其来源&#xff0c;内卷这个词的出现&#xff0c;是伴随着996开始讨论的。很不幸&#xff0c;996、福报等等这些词的重灾区和源头就是计算机/互联网行业。…

学了一个礼拜 JavaScript 为什么还是学不会?

前言 首先从你的描述里面我先以我的主观臆断来猜测一下你是没有任何编程的基础的&#xff0c;Js按理来说在各语言中并不是非常难学&#xff0c;当然如果你是纯新手入门&#xff0c;那么确实前期需要时间来沉淀一下语法&#xff0c;一个礼拜的话&#xff0c;按理来说应该是在沉…

JVM基础篇-StringTable

StringTable 特性 常量池中的字符串仅是符号&#xff0c;第一次用到时才变为对象 利用串池的机制&#xff0c;来避免重复创建字符串对象 字符串变量拼接的原理是 StringBuilder &#xff08;1.8&#xff09; 字符串常量拼接的原理是编译期优化 可以使用 intern 方法&#…

利用大数据分析工具,实现多场景可视化数据管理

官方使用文档 https://yanhuang.yuque.com/staff-sbytbc/rb5rur? 准备服务器环境 购买服务器 购买腾讯云服务器&#xff0c;1300 元新人价&#xff0c;一年时间 ●4核16G内存 ●CentOS 6.7 &#xff08;补充说明&#xff1a;最新的 2.7.1 GA 版本&#xff0c;8G 内存也是可以…

TCP的三次握手四次挥手

TCP的三次握手和四次挥手实质就是TCP通信的连接和断开。 三次握手&#xff1a;为了对每次发送的数据量进行跟踪与协商&#xff0c;确保数据段的发送和接收同步&#xff0c;根据所接收到的数据量而确认数据发送、接收完毕后何时撤消联系&#xff0c;并建立虚连接。 四次挥手&a…

flyway快速入门

flyway快速入门 一、flyway是什么&#xff1f;二、flyway使用目的1. 使用原因&#xff1a;2. 举个例子&#xff1a; 三、flyway工作原理四、flyway使用约定和命名规则1. 数据库版本文件整体约定2. 数据库版本文件夹管理约定3. 数据库版本文件命名约定4. 禁止项 五、flyway配置和…

微信多开(双开三开均可,且不局限于微信,其他设备亦可)

1.鼠标右键“微信”&#xff0c;属性 如上图&#xff0c;自动选取的&#xff0c;别动&#xff0c;然后CtrlC,,,,结果如下 "C:\Program Files (x86)\Tencent\WeChat\WeChat.exe" 2.创建文本&#xff0c;电脑桌面空白处单击&#xff0c;新建&#xff0c;文本档案&#…