当前位置: 首页 > news >正文

C++20协程详解

文章目录

    • 什么是协程
    • 为什么需要协程
    • 什么时候使用协程
    • 协程的类别
    • C++20的协程
    • 协程的使用
      • 关键字
      • co_wait
      • 框架
      • 一阶段完成
      • 数据交换
      • co_yield
      • co_return

什么是协程

我们在学习编程的过程中,逐渐从单线程,到多线程,再到异步编程和并发处理

这些异步与并发的任务不断增加,导致回调结构会变得复杂,为了提高代码的可读性和可维护性,协程(Coroutine)就被引入了

用来简化整个过程的操作

协程是一种特殊的函数,可以在中途暂停,并在稍后恢复执行

与线程不同,协程不会占用独立的系统资源,而是可以由程序直接控制何时暂停和恢复

因此协程其实就是属于某个具体的线程的,可以理解为一个单独的函数,一个单独的执行流,好处就是没有线程的上下文切换的消耗,协程的调度也是自行实现的,会更加灵活

请添加图片描述

为什么需要协程

简单理解的话,可以理解为,虽然线程已经能够完成绝大部分的任务了,但是线程的粒度还不够精细

举个例子

在网络中,我们调用read函数读取数据,如果缓冲区没有数据的话,整个线程就会一直阻塞到缓冲区有数据,虽然我们可以采用IO多路复用来避免这个问题,但是这也是一种消耗,而且写起来实在不合直觉

如果能把线程细分为更多的子任务就好了,我们可以主动控制多个子任务进行交替执行

假设read函数是一个单独的子任务,当read函数发现缓冲区没有内容时,则直接“阻塞”,让出这个线程的执行权,让线程去执行其他的任务

当可读条件满足时,在去唤醒read的子任务,从上次read“阻塞”的地方继续执行

这里的“阻塞”打了引号,因为并非是真的阻塞,而是相当于按下了暂停键,整个线程仍然是没有阻塞的

这样线程的并发就进一步提高了,这里的子任务也可以理解为是一个函数,我们只需要把当前函数的栈空间和寄存器状态保留即可

什么时候使用协程

主要是IO密集型的任务需要,例如网络请求,爬虫,文件读取,web框架等等

对于计算密集型任务还是别用了,因为本身就不需要大量的线程切换,还增加了协程切换的开销

协程的类别

协程在不同的语言实现也不太一样,Python,Java,Go,C++20都支持协程,大致可以分为两大类

  • 有栈协程(stackful),也就是类似于内核的线程,协程拥有自己的栈空间,不同协程的切换需要切换对应栈的上下文,只是操作系统不会陷入内核态,例如goroutine,libco
  • 无栈协程(stackless),这种就是把上下文会放到公共内存,切换的时候使用状态机来切换,而不用切换上下文,比有栈协程要清凉,C++20,Rust,JavaScript都是这种

这里的有栈和无栈指的是协程之间是否存在调用栈的关系

除此之外还有

  • 对称协程,允许协程之间互相调用,可以直接切换到其他的协程,而不必须返回到调用他的协程,可以写你刚才协程环和协程网
  • 非对称协程,也叫做一对多的协程,每个协程的控制权只能返回给直接调用他的协程,这种调用关系是单向的

C++20的协程

C++20的协程是属于无栈协程,非对称协程,对协程的实现的底层原理实际上是通过关键字展开,模板来实现的

协程的使用

关键字

主要的关键字有三个

  • co_await:暂停协程的执行,等待某个任务的完成
  • co_return:返回结果
  • co_yield:生成一个中间结果,暂停协程

当一个函数中出现了这三个关键字中的任意一个时,这个函数就会被判定为协程了

我们来写一个简单的协程猜数字,先大概看一下这是个什么东西

可以的话请跟着思路一起写,而非直接复制完整代码来看,这里还是比较复杂的

co_wait

框架

我们先搭一个基本的框架

#include <coroutine>
#include <iostream>
using namespace std;struct CoRet
{
};struct Input
{
};CoRet Guess()
{Input input;int g = co_await input;cout << "协程:你猜的是 " << g << endl;
}int main()
{auto ret = Guess(); cout << "主函数:猜一猜" << endl;return 0;
}

这里我们定义了两个类,分别是输入和输出,和一个协程Guess,怎么看这是一个协程能,因为其中用来co_await

有些奇怪的点是,Guess没有人恶化的返回语句,但是他可以拥有返回类型,这个返回类型就包含了这个协程的管理句柄,还有其中的一些回调函数

这时候编译器提示我们,他在CoRet这个返回值类型中找不到promise_type这个类

编译器需要我们提供promise_type这个类来对协程进行管理,他会在Guess开始执行是自动初始化promise_type这个类

接下来我们添加上相应的代码

/// @brief 协程函数的返回值类型
struct CoRet
{/// @brief 用于控制协程的运行,协程内外数据的交换struct promise_type{};
};
/// 虽然这个函数没有任何的返回语句,但是他仍然可以拥有返回类型
/// 这个返回类型就包含了这个协程的管理句柄,还有一些回调函数
CoRet Guess()
{// 在这里编译器会隐含的创建一个对象 当然这里是简化过后的// CoRet::promise_type promise;Input input;int g = co_await input; cout << "协程:你猜的是 " << g << endl;
}

然后这里编译器优惠提示我们, struct promise_type 里面少了一些函数,我们一一介绍

        suspend_never initial_suspend(){return {};}suspend_never final_suspend() noexcept{return {};}/// @brief 异常处理void unhandled_exception(){}

首先是这三个 前两个是当协程调用之前和调用结束之后是否需要暂停等待

如何区别呢,就是用suspend_never和suspend_always这两个提供给我们的类,这两个类是可以被直接co_await的对象

suspend_never 不暂停 继续运行

suspend_always 暂停等待 执行逻辑跳回到调用的地方

unhandled_exception当协程出现异常时的处理函数

还少了一个是

        CoRet get_return_object(){return{coroutine_handle<promise_type>::from_promise(*this)};}

这个函数是用于获取 协程暂停时返回的对象的,这个对象可以是任意一个对象

但是更加通用的对象其实是返回这个协程的管理句柄,不然没有办法管理协程了就

这里用的就是coroutine_handle<promise_type>类型作为句柄

需要注意的是,这个句柄是需要作为返回值的成员变量的,因此整个CoRet类就是长这样

/// @brief 协程函数的返回值类型
struct CoRet
{/// @brief 用于控制协程的运行,协程内外数据的交换struct promise_type{/// @return 提供了两个可以被 co_await 的对象/// suspend_never 不暂停 继续运行/// suspend_always 暂停等待 执行逻辑跳回到调用的地方suspend_never initial_suspend(){return {};}suspend_never final_suspend() noexcept{return {};}/// @brief 异常处理void unhandled_exception(){}/// @note 这个函数是用于获取协程暂停时返回的对象/// 这里其实可以返回的是一个任意对象/// 但是没有办法通过协程的返回值来控制协程的进行/// 最一般的方式是这样的 利用 promise 对象返回一个控制协程运行的句柄CoRet get_return_object(){return{coroutine_handle<promise_type>::from_promise(*this)};}};// 控制协程运行的句柄coroutine_handle<promise_type> _h; 
};

补全编译器给我们优化的协程就是这样的

/// @brief 
/// @return 协程函数的返回值类型
/// @note
/// 虽然这个函数没有任何的返回语句,但是他仍然可以拥有返回类型
/// 这个返回类型就包含了这个协程的管理句柄,还有一些回调函数
CoRet Guess()
{// 在这里编译器会隐含的创建一个对象 当然这里是简化过后的// CoRet::promise_type promise;// 利用对象产生一个返回的对象,每一次协程被暂停,返回的就是这个对象// CoRet ret = promise.get_return_object();// 用创建的对象 co_await initial_suspend 的结果// co_await promise.initial_suspend();Input input;int g = co_await input; // 运行到这里时跳转出去cout << "协程:你猜的是 " << g << endl;// co_await promise.final_suspend();}

然后编译器又提示我们Input类也少东西了

分别是await_ready,await_suspend,await_resume这三个函数

第一个await_ready表示,当遇到co_await 时 是否需要暂停当前协程并跳转回去 如果不需要暂停跳转则返回true 如果需要暂停等待则返回false

    bool await_ready(){return false;}

第二个await_suspend是,在协程即将要被暂停时可以执行的行为

是这样写的

    void await_suspend(coroutine_handle<CoRet::promise_type> h){// h.promise(); 这里协程管理句柄的promise成员函数返回的对象的作用// 其实就是获取协程的管理对象}

参数是当前协程的管理句柄 在即将暂停之间,我们可以得到管理句柄,并且执行一些操作

这个函数的返回值指的是 在这个协程在暂停之后,代码执行要跳转到的地方

如果是 void 则跳转到调用这个协程的地方

如果是 其他协程的句柄 则跳转到对应协程继续运行

类似于析构函数和构造函数

调用子类的构造函数时,也必定会自动调用父类的构造函数

这里就是 在遇到暂停之后,可以使用void返回到原来调用协程的地方,也可以继续调用其他协程

不同之处就是这里需要显式写出来

第三个是await_resume, 在 co_wait 需要一个返回值的时候 才需要写

这里的返回值类型是可以自己设定的,可以代表不同的含义

    int await_resume(){return 0;}

一阶段完成

这时候编译器也不再提示什么报错信息了

完整代码是这样

#include <coroutine>
#include <iostream>
using namespace std;/// @brief 协程函数的返回值类型
struct CoRet
{/// @brief 用于控制协程的运行,协程内外数据的交换struct promise_type{/// @return 提供了两个可以被 co_await 的对象/// suspend_never 不暂停 继续运行/// suspend_always 暂停等待 执行逻辑跳回到调用的地方suspend_never initial_suspend(){return {};}suspend_never final_suspend() noexcept{return {};}/// @brief 异常处理void unhandled_exception(){}/// @note 这个函数是用于获取协程暂停时返回的对象/// 这里其实可以返回的是一个任意对象/// 但是没有办法通过协程的返回值来控制协程的进行/// 最一般的方式是这样的 利用 promise 对象返回一个控制协程运行的句柄CoRet get_return_object(){return{coroutine_handle<promise_type>::from_promise(*this)};}};// 控制协程运行的句柄//  _h.resume() -> 继续运行协程, _h() -> 也是的coroutine_handle<promise_type> _h; 
};struct Input
{/// @brief 当遇到 co_await 时 是否需要暂停当前协程并跳转回去/// @return 不需要暂停跳转返回true | 需要暂停等待返回falsebool await_ready(){return false;}/// @brief 定义在协程即将要被暂停时的一些行为/// @param h 当前协程的管理句柄/// @note 在即将暂停之间,我们可以得到管理句柄,并且执行一些操作/// @return 这个函数的返回值指的是 在这个协程在暂停之后,代码执行要跳转到的地方/// 如果是 void 则跳转到调用这个协程的地方/// 如果是 其他协程的句柄 则跳转到对应协程继续运行/// 类似于析构函数和构造函数 /// 调用子类的构造函数时,也必定会自动调用父类的构造函数/// 这里就是 在遇到暂停之后,可以使用void返回到原来调用协程的地方,也可以继续调用其他协程/// 不同之处就是这里需要显式写出来void await_suspend(coroutine_handle<CoRet::promise_type> h){// h.promise(); 这里协程管理句柄的promise成员函数返回的对象的作用// 其实就是获取协程的管理对象}/// @brief 在 co_wait 需要一个返回值的时候 才需要写/// @return 这里的返回值类型是可以自己设定的,可以代表不同的含义int await_resume(){return 0;}
};/// @brief 
/// @return 协程函数的返回值类型
/// @note
/// 虽然这个函数没有任何的返回语句,但是他仍然可以拥有返回类型
/// 这个返回类型就包含了这个协程的管理句柄,还有一些回调函数
CoRet Guess()
{// 在这里编译器会隐含的创建一个对象 当然这里是简化过后的// CoRet::promise_type promise;// 利用对象产生一个返回的对象,每一次协程被暂停,返回的就是这个对象// CoRet ret = promise.get_return_object();// 用创建的对象 co_await initial_suspend 的结果// co_await promise.initial_suspend();Input input;int g = co_await input; // 运行到这里时跳转出去// co_await 后面可以加的是一个对象 但是也需要加一点东西cout << "协程:你猜的是 " << g << endl;// co_await promise.final_suspend();}int main()
{auto ret = Guess(); // 运行到 co_await 时跳转回来cout << "主函数:猜一猜" << endl;ret._h.resume(); // 运行到这里时跳转回去return 0;
}

运行结果是这样的

请添加图片描述

我们可以画一下协程和主函数的调用关系

请添加图片描述

数据交换

这里我们可以发现,在CoRet这对象里的promise对象就是沟通协程内外的桥梁

一个自然的想要给协程输入的方法其实就是给这个promise_type对象中新建一个成员变量,然后在Input对象里面存一个promise_type新增的成员变量的地址

在调用await_suspend()时就使用promise对象中的地址对Input对象中的指针赋值,然后再在await_resume()中返回即可

显然这种方法曲折又复杂,没什么人喜欢的

这样我们创建一个新的类 来作为输入,将这个类作为协程的成员变量

这个类在main中初始化,然后通过参数传递给函数,再用协程的Input接收这个参数并初始化协程,就能够完成协程和main函数之间的参数传递了

struct Note
{int num;Note() : num(100) {}
};
struct Input
{Note &in;bool await_ready(){cout << "await_ready" << endl;return false;}void await_suspend(coroutine_handle<CoRet::promise_type> h){cout << "await_suspend" << endl;}int await_resume(){cout << "await_resume" << endl;return in.num;}
};
CoRet Guess(Note &note)
{Input input{note};int g = co_await input; cout << "协程:你猜的是 " << g << endl;
}
int main()
{Note note;auto ret = Guess(note); cout << "主函数:猜一猜" << endl;ret._h.resume();return 0;
}

co_yield

这个关键字其实就等价于 co_await promise.yield_value(expr)

这里我们再加一点东西

在Guess开始的时候,产生一个1到114的随机数 int res = (rand() % 114 + 1);

然后从协程外面读取一个数字

再调用co_yield对比较数字的大小

co_yield (res > g ? 1 : (res == g ? 0 : -1));

这里三目运算符的结果无非就是-1 0 1这三种

就相当于调用了这样的函数 co_await promise.yield_value(-1\0\1);

但是我们并没有实现yield_value 因此需要写一下 他的输入其实就是co_yield后面的那个表达式 返回值则是用于区分执行完之后是否需要暂停等待 如果是suspend_never则不等待 如果是suspend_always则进行等待

为了保存这个co_yield输入的参数,我们需要在promise对象中添加一个参数

        suspend_always yield_value(int r){out = r;return {};}int out;

这样当协程比完大小之后,就会把结果赋值到协程管理句柄的out中

然后在主函数中就可以直接取到结果了

int main()
{srand(time(nullptr));Note note;auto ret = Guess(note); // 运行到 co_await 时跳转回来cout << "主函数:猜一猜" << endl;note.num = 10;ret._h.resume(); // 运行到这里时跳转回去cout << "主函数:猜的结果是:" << ((ret._h.promise().out == 1) ? "猜大了" : ((ret._h.promise().out == 0) ? "猜对了" : "猜小了"))<< endl;return 0;
}

请添加图片描述

co_return

这个co_return其实和co_yield是差不多的

他也是会调用一个return_value的函数,不同的是,他会在管理句柄中标记这个协程是否已经完成

在协程管理句柄中有一个成员函数是done(),可以获取协程是否执行完成的状态

在这个例子里面,我们想要获取res,同样就是在promise对象中添加一个参数 然后进行赋值就行

        void return_value(int r){res = r;}int res;

在Guess的最后调用co_return res;就会自动转化为co_await return_value(res);

然后在main函数中就可以读取到了

完整代码

#include <coroutine>
#include <iostream>
using namespace std;/// @brief 协程函数的返回值类型
struct CoRet
{/// @brief 用于控制协程的运行,协程内外数据的交换struct promise_type{/// @return 提供了两个可以被 co_await 的对象/// suspend_never 不暂停 继续运行/// suspend_always 暂停等待 执行逻辑跳回到调用的地方suspend_never initial_suspend(){cout << "initial_suspend" << endl;return {};}suspend_never final_suspend() noexcept{cout << "final_suspend" << endl;return {};}/// @brief 异常处理void unhandled_exception(){}/// @note 这个函数是用于获取协程暂停时返回的对象/// 这里其实可以返回的是一个任意对象/// 但是没有办法通过协程的返回值来控制协程的进行/// 最一般的方式是这样的 利用 promise 对象返回一个控制协程运行的句柄CoRet get_return_object(){cout << "get_return_object" << endl;return {coroutine_handle<promise_type>::from_promise(*this)};}suspend_always yield_value(int r){out = r;return {};}int out;void return_value(int r){res = r;}int res;};// 控制协程运行的句柄//  _h.resume() -> 继续运行协程, _h() -> 也是的coroutine_handle<promise_type> _h;
};struct Note
{int num;
};struct Input
{Note &in;/// @brief 当遇到 co_await 时 是否需要暂停当前协程并跳转回去/// @return 不需要暂停跳转返回true | 需要暂停等待返回falsebool await_ready(){cout << "await_ready" << endl;return false;}/// @brief 定义在协程即将要被暂停时的一些行为/// @param h 当前协程的管理句柄/// @note 在即将暂停之间,我们可以得到管理句柄,并且执行一些操作/// @return 这个函数的返回值指的是 在这个协程在暂停之后,代码执行要跳转到的地方/// 如果是 void 则跳转到调用这个协程的地方/// 如果是 其他协程的句柄 则跳转到对应协程继续运行/// 类似于析构函数和构造函数/// 调用子类的构造函数时,也必定会自动调用父类的构造函数/// 这里就是 在遇到暂停之后,可以使用void返回到原来调用协程的地方,也可以继续调用其他协程/// 不同之处就是这里需要显式写出来void await_suspend(coroutine_handle<CoRet::promise_type> h){cout << "await_suspend" << endl;// h.promise(); 这里协程管理句柄的promise成员函数返回的对象的作用// 其实就是获取协程的管理对象}/// @brief 在 co_wait 需要一个返回值的时候 才需要写/// @return 这里的返回值类型是可以自己设定的,可以代表不同的含义int await_resume(){cout << "await_resume" << endl;return in.num;}};/// @brief
/// @return 协程函数的返回值类型
/// @note
/// 虽然这个函数没有任何的返回语句,但是他仍然可以拥有返回类型
/// 这个返回类型就包含了这个协程的管理句柄,还有一些回调函数
CoRet Guess(Note &note)
{// 在这里编译器会隐含的创建一个对象 当然这里是简化过后的// CoRet::promise_type promise;// 利用对象产生一个返回的对象,每一次协程被暂停,返回的就是这个对象// CoRet ret = promise.get_return_object();// 用创建的对象 co_await initial_suspend 的结果// co_await promise.initial_suspend();int res = (rand() % 114) + 1;Input input{note};int g = co_await input; // 运行到这里时跳转出去// co_await 后面可以加的是一个对象 但是也需要加一点东西cout << "协程:你猜的是 " << g << endl;co_yield (res > g ? 1 : (res == g ? 0 : -1));co_return res;// co_await promise.final_suspend();
}int main()
{srand(time(nullptr));Note note;auto ret = Guess(note); // 运行到 co_await 时跳转回来cout << "主函数:猜一猜" << endl;note.num = 10;ret._h.resume(); // 运行到这里时跳转回去cout << "主函数:猜的结果是:" << ((ret._h.promise().out == 1) ? "猜大了" : ((ret._h.promise().out == 0) ? "猜对了" : "猜小了"))<< endl;cout << "主函数:随机数是:" << ret._h.promise().res << endl;return 0;
}

相关文章:

C++20协程详解

文章目录 什么是协程为什么需要协程什么时候使用协程协程的类别C20的协程协程的使用关键字co_wait框架一阶段完成数据交换co_yieldco_return 什么是协程 我们在学习编程的过程中&#xff0c;逐渐从单线程&#xff0c;到多线程&#xff0c;再到异步编程和并发处理 这些异步与并…...

Chromium 中chrome.system.display扩展接口定义c++

一、chrome.system.display 使用 system.display API 查询展示元数据。 权限 system.display 类型 ActiveState Chrome 117 及更高版本 用于指示系统是否检测到和使用显示屏的枚举。如果系统未检测到显示屏&#xff08;可能断开连接&#xff0c;或因睡眠模式等原因而被视…...

容器docker的ulimit

Ulimit 在linux里ulimit命令可以对shell生成的进程的资源进行限制。 常用的ulimit限制 打开文件句柄数core文件大小设置进程能够消耗的虚拟内存设置用户能够打开的进程数目 不太常用的ulimit限制 设置数据段的最大值.单位:kbytes 设置创建文件的最大值.单位:blocks 设置在…...

一、HTML

一、基础概念 1、浏览器相关知识 这五个浏览器市场份额都非常大&#xff0c;且都有自己的内核。 什么是内核&#xff1a; 内核是浏览器的核心&#xff0c;用于处理浏览器所得到的各种资源。 例如&#xff0c;服务器发送图片、视频、音频的资源&#xff0c;浏览…...

使用Geekbench6软件对真实和虚拟的苹果桌面系统(macOS)进行打分比较

前言 感觉VMWare安装的MacOS使用起来非常的慢&#xff0c;所以特意用打分软件GeekBench进行了评测。 一、Geekbench的安装 可以从官网直接进行下载&#xff0c; 链接是&#xff1a; 二、Geekbench的直接使用 2.1、真机的信息 2.2、虚拟机的信息 三、打分的比较 3.1、真机…...

lua入门教程:随机数

在Lua中&#xff0c;生成随机数是通过math库中的math.random函数来实现的。这个函数可以生成一个[0, 1)区间内的随机浮点数。如果你需要生成其他范围内的随机数&#xff0c;或者需要整数类型的随机数&#xff0c;可以通过一些简单的数学运算来调整math.random的输出。 以下是如…...

华为大咖说 | 浅谈智能运维技术

本文分享自华为云社区&#xff1a;华为大咖说 | 浅谈智能运维技术-云社区-华为云 本文作者&#xff1a;李文轩 &#xff08; 华为智能运维专家 &#xff09; 全文约2695字&#xff0c;阅读约需8分钟 在大数据、人工智能等新兴技术的加持下&#xff0c;智能运维&#xff08;AI…...

creo toolkit二次开发学习之获取任意选择模型作为元件,并进行获取约束等

获取任意选择模型作为元件进行操作前&#xff0c;先了解组件路径和程序集的构成&#xff1a;creo toolkit二次开发学习之程序集&#xff08;ProAsmcomp&#xff09;和装配体组件路径对象&#xff08;ProAsmcomppath&#xff09;-CSDN博客 代码如下 ProError test1() {ProError…...

sanitize-html 防止 XSS(跨站脚本攻击)

sanitize-html 是一个用于清理和验证 HTML 的 JavaScript 库&#xff0c;主要用于防止 XSS&#xff08;跨站脚本攻击&#xff09;。它允许你定义一套规则来决定哪些 HTML 标签和属性是可以被信任的&#xff0c;从而确保用户输入的内容不会包含潜在的恶意代码。 主要功能 HTML…...

【JavaEE】文件io

目录 文件类型 File概述 属性 构造方法 常用方法 Reader Writer InputStream OutputStream 字节流转字符流 通过Scanner读取InputStream 通过PrintWriter转换outputstream 示例 文件类型 从编程的角度看&#xff0c;文件类型主要就是两大类 文本&#xff08;文…...

FlinkPipelineComposer 详解

FlinkPipelineComposer 详解 原文 背景 在flink-cdc 3.0中引入了pipeline机制&#xff0c;提供了除Datastream api/flink sql以外的一种方式定义flink 任务 通过提供一个yaml文件&#xff0c;描述source sink transform等主要信息 由FlinkPipelineComposer解析&#xff0c…...

蓝桥杯-洛谷刷题-day2(C++)

目录 1.小写字母与大写字母的转换 2.使用string&#xff08;额外开一章持续补充&#xff09; i.访问字符串最后一位 3.保留N位小数输出 i.C侧 ii.C语言侧 iii.总结 4.高精度相加 i.各种数据类型转字符型 ii.三元运算符 iii.循环条件中的carry 1.小写字母与大写字母的…...

16008.行为树(五)-自定义数据指针在黑板中的传递

文章目录 1.1 背景1.2 xml文件定义1.3 代码实现1.3 执行结果1.1 背景 自定义数据结构指针,通过黑板的形式,在树的节点中进行指针的传递。 1.2 xml文件定义 xhome@ubuntu:~/opt/groot_pro$ cat unit_t1.xml<?xml version="1.0" encoding="UTF-8"?&…...

javascript Vue

DOM对象 什么是DOM DOM(Document Object Model)&#xff1a;文档对象模型&#xff0c;就是Javascript将HTML文档的各个组成部分封装为对象&#xff0c;通过修改HTML元素的内容和样式动态改变页面。 如何获取DOM对象 获取DOM中的元素对象&#xff08;Element对象/标签&…...

《揭秘观察者模式:作用与使用场景全解析》

在软件开发的世界中&#xff0c;设计模式就像是建筑师手中的蓝图&#xff0c;指导着软件系统的构建。其中&#xff0c;观察者模式是一种极为重要且广泛应用的设计模式。今天&#xff0c;我们就来深入探讨一下观察者模式的作用和使用场景。 一、观察者模式是什么&#xff1f; …...

【QT常用技术讲解】优化网络链接不上导致qt、qml界面卡顿的问题

前言 qt、qml项目经常会涉及访问MySQL数据库、网络服务器&#xff0c;并且界面打开时的初始化过程就会涉及到链接Mysql、网络服务器获取数据&#xff0c;如果网络不通&#xff0c;卡个几十秒&#xff0c;会让用户觉得非常的不爽&#xff0c;本文从技术调研的角度讲解解决此类问…...

下划线命名json数组转java对象

/*** 将驼峰式命名的字符串转换为下划线方式* @param camelCase* @return*/ private static String toUnderlineCase(String camelCase) {return StrUtil.toUnderlineCase(camelCase); }/*** 下划线-赋值给-驼峰* @param source 源数据* @param target 目标数据*/ public stati…...

实测运行容器化Nginx服务器

文章目录 前言一、拉取Nginx镜像二、创建挂载目录三、运行容器化Nginx服务器四、访问网页测试 总结 前言 运行容器化Nginx服务器&#xff0c;首先确保正确安装docker&#xff0c;并且已启动运行&#xff0c;具体安装docker方法见笔者前面的博文《OpenEuler 下 Docker 安装、配…...

显示器接口种类 | 附图片

显示器接口类型主要包括VGA、DVI、HDMI、DP和USB Type-C等。 VGA、DVI、HDMI、DP和USB Type-C 1. 观察 VGA接口:15针 DP接口&#xff1a;在DP接口旁&#xff0c;都有一个“D”型的标志。 电脑主机&#xff1a;DP(D) 显示器&#xff1a;VGA(15针) Ref https://cloud.tenc…...

C++初阶——list

一、什么是list list是一个可以在序列的任意位置进行插入和删除的容器&#xff0c;并且可以进行双向迭代。list的底层是一个双向链表&#xff0c;双向链表可以将它们包含的每个元素存储在不同且不相关的存储位置。通过将每个元素与前一个元素的链接和后一个元素的链接关联起来&…...

HTML 语义化

目录 HTML 语义化HTML5 新特性HTML 语义化的好处语义化标签的使用场景最佳实践 HTML 语义化 HTML5 新特性 标准答案&#xff1a; 语义化标签&#xff1a; <header>&#xff1a;页头<nav>&#xff1a;导航<main>&#xff1a;主要内容<article>&#x…...

Objective-C常用命名规范总结

【OC】常用命名规范总结 文章目录 【OC】常用命名规范总结1.类名&#xff08;Class Name)2.协议名&#xff08;Protocol Name)3.方法名&#xff08;Method Name)4.属性名&#xff08;Property Name&#xff09;5.局部变量/实例变量&#xff08;Local / Instance Variables&…...

C++ 基础特性深度解析

目录 引言 一、命名空间&#xff08;namespace&#xff09; C 中的命名空间​ 与 C 语言的对比​ 二、缺省参数​ C 中的缺省参数​ 与 C 语言的对比​ 三、引用&#xff08;reference&#xff09;​ C 中的引用​ 与 C 语言的对比​ 四、inline&#xff08;内联函数…...

多种风格导航菜单 HTML 实现(附源码)

下面我将为您展示 6 种不同风格的导航菜单实现&#xff0c;每种都包含完整 HTML、CSS 和 JavaScript 代码。 1. 简约水平导航栏 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport&qu…...

python执行测试用例,allure报乱码且未成功生成报告

allure执行测试用例时显示乱码&#xff1a;‘allure’ &#xfffd;&#xfffd;&#xfffd;&#xfffd;&#xfffd;ڲ&#xfffd;&#xfffd;&#xfffd;&#xfffd;ⲿ&#xfffd;&#xfffd;&#xfffd;Ҳ&#xfffd;&#xfffd;&#xfffd;ǿ&#xfffd;&am…...

中医有效性探讨

文章目录 西医是如何发展到以生物化学为药理基础的现代医学&#xff1f;传统医学奠基期&#xff08;远古 - 17 世纪&#xff09;近代医学转型期&#xff08;17 世纪 - 19 世纪末&#xff09;​现代医学成熟期&#xff08;20世纪至今&#xff09; 中医的源远流长和一脉相承远古至…...

Kafka主题运维全指南:从基础配置到故障处理

#作者&#xff1a;张桐瑞 文章目录 主题日常管理1. 修改主题分区。2. 修改主题级别参数。3. 变更副本数。4. 修改主题限速。5.主题分区迁移。6. 常见主题错误处理常见错误1&#xff1a;主题删除失败。常见错误2&#xff1a;__consumer_offsets占用太多的磁盘。 主题日常管理 …...

【若依】框架项目部署笔记

参考【SpringBoot】【Vue】项目部署_no main manifest attribute, in springboot-0.0.1-sn-CSDN博客 多一个redis安装 准备工作&#xff1a; 压缩包下载&#xff1a;http://download.redis.io/releases 1. 上传压缩包&#xff0c;并进入压缩包所在目录&#xff0c;解压到目标…...

aurora与pcie的数据高速传输

设备&#xff1a;zynq7100&#xff1b; 开发环境&#xff1a;window&#xff1b; vivado版本&#xff1a;2021.1&#xff1b; 引言 之前在前面两章已经介绍了aurora读写DDR,xdma读写ddr实验。这次我们做一个大工程&#xff0c;pc通过pcie传输给fpga&#xff0c;fpga再通过aur…...

SpringCloud优势

目录 完善的微服务支持 高可用性和容错性 灵活的配置管理 强大的服务网关 分布式追踪能力 丰富的社区生态 易于与其他技术栈集成 完善的微服务支持 Spring Cloud 提供了一整套工具和组件来支持微服务架构的开发,包括服务注册与发现、负载均衡、断路器、配置管理等功能…...