C++20 协程(coroutine)入门

文章目录

  • 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 中的协程,便可以说了解了协程的底层原理,处理其他语言中的协程也就游刃有余了。

### (未完待续)

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

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

相关文章

如何在 Ubuntu 上部署 ONLYOFFICE 协作空间社区版?

ONLYOFFICE 协作空间是一个在线协作平台&#xff0c;帮助您更好地与客户、业务合作伙伴、承包商及第三方进行文档协作。今天我们来介绍一下&#xff0c;如何在 Ubuntu 上安装协作空间的自托管版。 ONLYOFFICE 协作空间主要功能 使用 ONLYOFFICE 协作空间&#xff0c;您可以&am…

安全文件传输的重要性及其对企业的影响

在当今的信息时代&#xff0c;企业之间的文件传输已经成为日常工作的重要组成部分。无论是在商务合作、人力资源还是财务审计等方面&#xff0c;文件传输都发挥着关键的作用。然而&#xff0c;随着网络技术的发展&#xff0c;网络安全问题也日益突出&#xff0c;泄漏、篡改、丢…

复习之selinux的管理

一、什么是selinux? SELinux&#xff0c;Security Enhanced Linux 的缩写&#xff0c;也就是安全强化的 Linux&#xff0c;是由美国国家安全局&#xff08;NSA&#xff09;联合其他安全机构&#xff08;比如 SCC 公司&#xff09;共同开发的&#xff0c;旨在增强传统 Linux 操…

Jmeter 压测工具使用手册[详细]

1. jemter 简介 jmeter 是 apache 公司基于 java 开发的一款开源压力测试工具&#xff0c;体积小&#xff0c;功能全&#xff0c;使用方便&#xff0c;是一个比较轻量级的测试工具&#xff0c;使用起来非常简 单。因为 jmeter 是 java 开发的&#xff0c;所以运行的时候必须先…

paddlenlp:社交网络中多模态虚假媒体内容核查

初赛之环境配置篇 一、背景二、任务三、数据集1、初赛阶段2、评分标准 四、环境操作五、写在最后 一、背景 随着新媒体时代信息媒介的多元化发展&#xff0c;各种内容大量活跃在媒体内中&#xff0c;与此同时各类虚假信息也充斥着社交媒体&#xff0c;影响着公众的判断和决策。…

vue3—SCSS的安装、配置与使用

SCSS 安装 使用npm安装scss&#xff1a; npm install sass sass-loader --save-dev 配置 配置到全局 &#x1f31f;附赠代码&#x1f31f; css: {preprocessorOptions: {scss: {additionalData:import "./src/Function/Easy_I_Function/Echarts/ToSeeEcharts/utill.…

防火墙规则分析管理

防火墙规则在高效的网络安全管理中起着至关重要的作用&#xff0c;在添加规则之前&#xff0c;确保提议的新规则不会对网络产生负面影响至关重要。 通过防火墙规则影响分析&#xff0c;安全管理员可以详细了解添加新规则的可能影响&#xff0c;防火墙规则影响分析的一个重要方…

智能卡通用安全检测指南 思度文库

范围 本标准规定了智能卡类产品进行安全性检测的一般性过程和方法。 本标准适用于智能卡安全性检测评估和认证。 规范性引用文件 下列文件对于本文件的应用是必不可少的。凡是注日期的引用文件&#xff0c;仅注日期的版本适用于本文件。凡是不注日期的引用文件&#xff0c;…

git仓库与本地暂存区的同步问题

向下同步 对于远程仓库的项目&#xff0c;初始化一个配置文件&#xff0c;配置远程仓库及相关信息&#xff0c;赋值远程仓库的地址&#xff0c;使用git pull命令即可拉取仓库代码。 git pull [remote_addr] 该部分完成向下同步 向上同步 向上同步时会遇到很多的问题&#xf…

6.3 填充和步幅

一.填充 1.作用&#xff1a; 为了防止丢失边缘像素。如240x240的像素图像&#xff0c;经过10层5x5卷积&#xff0c;变成了200x200像素。可以根据输出形状计算公式 (w-k1) x (h-k1)计算得出。 2.方法: 最常用的方法是填充0。如下&#xff1a; 3.公式&#xff1a;计算填充原…

matlab计算基础

目录 1. 创建矩阵和向量 2. 矩阵的基本运算 2.1 数乘 2.2 转秩 2.3 求逆 2.4 点积 2.5 拼接 3. 复数 4. 矩阵元素的引用 5.工作区中数据的保存和使用 1. 创建矩阵和向量 向量包括行向量和列向量&#xff0c;向量就是个特殊的矩阵&#xff0c;向量可看作C语言中的一维…

SpringBoot手撕登陆验证码-【JSB项目实战】

SpringBoot系列文章目录 SpringBoot知识范围-学习步骤【JSB系列之000】 文章目录 SpringBoot系列文章目录本系列校训 SpringBoot技术很多很多环境及工具&#xff1a;上效果图目前流行的验证码技术介绍在springBoot项目里如果手撕验证码然后private void createCaptch(String …

SpringBoot第31讲:SpringBoot集成ShardingJDBC - Sharding-JDBC简介和基于MyBatis的单库分表

SpringBoot第31讲&#xff1a;SpringBoot集成ShardingJDBC - Sharding-JDBC简介和基于MyBatis的单库分表 本文是SpringBoot第31讲&#xff0c;主要介绍分表分库&#xff0c;以及SpringBoot集成基于ShardingJDBCMyBatis的单库分表实践 文章目录 SpringBoot第31讲&#xff1a;Spr…

continue有什么作用

学习算法以来&#xff0c;break使用的比较多&#xff0c;continue使用的比较少&#xff0c;只知道break是跳出循环的作用,不知道continue有什么作用。 continue可以跳过本次循环&#xff0c;强制执行下一次循环。 比如这个代码 #include<iostream>using namespace std…

Swish - Mac 触控板手势窗口管理工具[macOS]

Swish for Mac是一款Mac触控板增强工具&#xff0c;借助直观的两指轻扫&#xff0c;捏合&#xff0c;轻击和按住手势&#xff0c;就可以从触控板上控制窗口和应用程序。 Swish for Mac又不仅仅只是一个窗口管理器&#xff0c;Swish具有28个易于使用的标题栏&#xff0c;停靠栏…

element+vue 之动态form

1.页面部分 <div v-for"(item,index) in formList" :key"index"><el-col :span"6" v-if"item.inputType0"><el-form-item :label"item.conditionName" :prop"item.conditionCode":rules"{req…

网络安全(黑客)自学就业

前段时间&#xff0c;遇到网友提问&#xff0c;说为什么我信息安全专业的找不到工作&#xff1f; 造成这个结果主要是有两大方面的原因。 第一个原因&#xff0c;求职者本身的学习背景问题。那这些问题就包括学历、学校学到的知识是否扎实&#xff0c;是否具备较强的攻防实战…

【C++】——模板

目录 泛型编程函数模板函数模板的概念函数模板格式&#xff1a;函数模板的原理函数模板的实例化模板参数的匹配原则 类模板类模板定义格式类模板的实例化 泛型编程 泛型编程&#xff1a;编写与类型无关的通用代码&#xff0c;是代码复用的一种手段。模板是泛型编程的基础 引例…

Go For Web:Golang http 包详解(源码剖析)

正文&#xff1a; Golang http 包详解&#xff08;源码剖析&#xff09; 前面小节我们认识了 Web 的工作方式&#xff0c;也成功用 Go 搭建了一个最简单的 Web 服务了解了 Golang 运行 Web 的原理。现在我们详细地去解剖以下 http 包&#xff0c;看看它如何实现整个过程的 Go…

【UEC++学习】UE网络 - Replication、RPC

1. UE网络架构 &#xff08;1&#xff09;UE的网络架构是SC&#xff08;Server - Client&#xff09;的模式&#xff0c;这种模式的优势&#xff1a;这种模式让所有客户端都在服务器端进行安全验证&#xff0c;这样可以有效的防止客户端上的作弊问题。 &#xff08;2&#xff…