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 mainimport ("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 iif __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 + 1def my_print(id):inner_coroutine = my_complex_task(id)# 繁琐:怎么拿到协程的返回值?last_yield = Nonefor 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.mzph.cn/news/24053.shtml

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

相关文章

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

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

SNAT和DNAT原理与应用

iptables的备份和还原 1.写在命令行当中的都是临时配置。 2.把我们的规则配置在 备份&#xff08;导出&#xff09;&#xff1a;iptables-save > /opt/iptables.bak 默认配置文件&#xff1a;/etc/sysconfig/iptables 永久配置&#xff1a;cat /opt/iptables.bak > /etc…

并查集练习—省份数量

上一篇中讲了并查集及其原理&#xff0c;在这篇文章中简单应用一下。如果对并查集不是很了解强烈建议先看上一篇。 题目&#xff1a; 有 n 个城市&#xff0c;其中一些彼此相连&#xff0c;另一些没有相连。如果城市 a 与城市 b 直接相连&#xff0c;且城市 b 与城市 c 直接相…

DP-GAN损失

在前面我们看了生成器和判别器的组成。 生成器损失公式&#xff1a; 首先将fake image 和真实的 image输入到判别器中&#xff1a; 接着看第一个损失&#xff1a;参数分别为fake image经过判别器的输出mask&#xff0c;和真实的label进行损失计算。对应于&#xff1a; 其中l…

捕捉时刻:将PDF文件中的图像提取为个性化的瑰宝(从pdf提取图像)

应用场景&#xff1a; 该功能的用途是从PDF文件中提取图像。这在以下情况下可能会很有用&#xff1a; 图片提取和转换&#xff1a;可能需要将PDF文件中的图像提取出来&#xff0c;并保存为单独的图像文件&#xff0c;以便在其他应用程序中使用或进行进一步处理。例如&#xff…

恺英网络宣布:与华为鸿蒙系统展开合作,将开发多款手游

8月5日消息&#xff0c;恺英网络宣布旗下子公司盛和网络参加了华为开发者大会&#xff08;HDC.Together&#xff09;游戏服务论坛&#xff0c;并在华为鸿蒙生态游戏先锋合作启动仪式上进行了亮相。恺英网络表示&#xff0c;将逐步在HarmonyOS上开发多款游戏&#xff0c;利用Har…

JVM 调优

点击下方关注我&#xff0c;然后右上角点击...“设为星标”&#xff0c;就能第一时间收到更新推送啦~~~ JVM调优是一项重要的任务&#xff0c;可以提高Java应用程序的性能和稳定性。掌握JVM调优需要深入了解JVM的工作原理、参数和配置选项&#xff0c;以及历史JVM参数的调整和优…

WMS仓库管理系统研发规划说明

01 产品背景 1.1 背景概述 aboss WMS东南亚仓库管理系统是一个基于BigSeller系统的使用基础上&#xff0c;加上多仓库的解决思路&#xff0c;解决入库业务、出库业务、仓库调拨、库存调拨和虚仓管理等功能&#xff0c;对批次管理、物料对应、库存盘点、质检管理、虚仓管理和即…

MYSQL进阶-事务的基础知识

1.什么是数据库事务&#xff1f; 就是把好几个sql语句打包成一个整体执行&#xff0c;要么全部成功&#xff0c;要么全部失败&#xff01;&#xff01;&#xff01; 事务是一个不可分割的数据库操作序列&#xff0c;也是数据库并发控制的基本单位&#xff0c;其执 行的结果必…

秋招算法备战第37天 | 738.单调递增的数字、968.监控二叉树、贪心算法总结

738. 单调递增的数字 - 力扣&#xff08;LeetCode&#xff09; 这个问题是关于找到一个小于或等于给定数字n的最大单调递增数字。 我们可以将数字n转换为字符数组&#xff0c;然后从左到右扫描&#xff0c;寻找第一个违反单调递增条件的位置。一旦找到这样的位置&#xff0c;…

安捷伦Agilent37719A通讯分析仪

安捷伦Agilent37719A通讯分析仪(131----4587---6435&#xff09; ATM和POS测试能力达到2.5 Gb/s OC-48、OC-48c、OC-12、OC-12c、OC-3c、OC-3、OC-1、STS-3、STS-3c、STS-1测试 保护切换时间测量 所有同步速率高达2.5 Gb/s的串联有效负载 SONET环翻转的全面直通模式操作 全开销…

【笔记】第94期-冯永吉-《湖仓集一体关键技术解读》-大数据百家讲坛-厦大数据库实验室主办20221022

https://www.bilibili.com/video/BV1714y1j7AU/?spm_id_from333.337.search-card.all.click&vd_sourcefa36a95b3c3fa4f32dd400f8cabddeaf

【数理知识】协方差,随机变量的的协方差,随机变量分别是单个数字和向量时的协方差

序号内容1【数理知识】自由度 degree of freedom 及自由度的计算方法2【数理知识】刚体 rigid body 及刚体的运动3【数理知识】刚体基本运动&#xff0c;平动&#xff0c;转动4【数理知识】向量数乘&#xff0c;内积&#xff0c;外积&#xff0c;matlab代码实现5【数理知识】协…

【雕爷学编程】MicroPython动手做(30)——物联网之Blynk 2

知识点&#xff1a;什么是掌控板&#xff1f; 掌控板是一块普及STEAM创客教育、人工智能教育、机器人编程教育的开源智能硬件。它集成ESP-32高性能双核芯片&#xff0c;支持WiFi和蓝牙双模通信&#xff0c;可作为物联网节点&#xff0c;实现物联网应用。同时掌控板上集成了OLED…

EXCEL里数值列如何显示序号?如何重新排序? 怎么取得排序后的序号?

目录 1 EXCEL里如何显示序号&#xff1f; 2 如何重新排序&#xff1f; 3 怎么取得排序后的序号&#xff1f; 3.1 rank() 的序号可能不连续 3.2 方法2&#xff1a;SUMPRODUCT((C7>C$7:C$12)/COUNTIF(C$7:C$12,C$7:C$12))1 EXCEL里如何显示序号&#xff1f;如何重新排序…

MySQL数据库面试题:如何定位慢查询?

MySQL数据库面试题&#xff1a;如何定位慢查询&#xff1f; 面试官&#xff1a;MySQL中&#xff0c;如何定位慢查询&#xff1f; 候选人&#xff1a;嗯~&#xff0c;我们当时做压测的时候有的接口非常的慢&#xff0c;接口的响应时间超过了2秒以上&#xff0c;因为我们当时的系…

Linux命令(59)之screen

linux命令之screen 1.screen介绍 linux命令screen是用来进行多窗口管理。 默认screen命令没有安装&#xff0c;安装命令(基于yum源)&#xff1a;yum install -y screen 2.screen用法 screen [参数] screen参数 参数说明-r恢复离线的screen作业-ls显示所有的screen作业 3.…

echarts 饼图的label放置于labelLine引导线上方

一般的饼图基础配置后长这样。 想要实现将文本放置在引导线上方&#xff0c;效果长这样 const options {// ...series: [{label: {padding: [0, -40],},labelLine: {length: 10,length2: 50,},labelLayout: {verticalAlign: "bottom",dy: -10,},},], };label.padd…

Zip压缩包密码忘记了,怎么办?

Zip压缩包设置了密码&#xff0c;解压的时候就需要输入正确对密码才能顺利解压出文件&#xff0c;正常当我们解压文件或者删除密码的时候&#xff0c;虽然方法多&#xff0c;但是都需要输入正确的密码才能完成。忘记密码就无法进行操作。 那么&#xff0c;忘记了zip压缩包的密…

CNN成长路:从AlexNet到EfficientNet(01)

一、说明 在 10年的深度学习中&#xff0c;进步是多么迅速&#xff01;早在 2012 年&#xff0c;Alexnet 在 ImageNet 上的准确率就达到了 63.3% 的 Top-1。现在&#xff0c;我们超过90%的EfficientNet架构和师生训练&#xff08;teacher-student&#xff09;。 如果我们在 Ima…