【Linux】进程间通信、匿名管道、进程池
一.什么是通信
进程间通信(Inter-Process Communication,IPC),是指在操作系统中,不同进程之间进行数据交换和同步的机制。由于每个进程通常拥有独立的内存空间,进程间无法直接访问对方的内存,因此需要通过特定的机制来实现通信和协作。
二.为什么要进程间通信
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同一份资源
- 通知事件:一个进程需要向一个或一组进程发送消息,通知它(它们)发生了某种时间(如进程终止需要通知父进程)
- 进程控制:有些进程希望完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
三.怎么进行进程间通信
因为进程具有独立性,所以进程无法互相访问对方的内存空间,所以为了让进程可以互相通信,前提就是让进程看到同一份资源,也就是同一份内存空间。而且这个内存空间不是由任意一个通信的进程提供的!
而在进程间通信刚被需要时,并没有独立为其开发方案和代码,而是在已有的Linux系统上,利用目前的系统特性,结合文件系统,开发出来一套基于文件系统的进程间通信方案——管道(pipe)。
四.匿名管道
匿名管道主要用于父子进程之间的通信。
我们知道,父进程在创建子进程后,子进程会拷贝父进程的pcb、页表、文件描述符表等等...
所以,当父进程打开了一个文件之后,再创建子进程,此时子进程和父进程都可以看到同一个内存资源——打开的文件。但是我们说了两个进程看到的资源不能是通信的进程提供的。所以为了实现通信,我们就有了管道的概念。
而起始我们在命令行操作中,早已经了解了管道操作:
ls | wc -l
我们执行的命令运行起来就是进程,而我们上面的操作起始就是进程间的通信。ls进程将自己获取信息通过管道交给了wc进程,由wc进程来统计文件数量。
0x1.管道的创建
这个管道是OS单独设计的内存级资源。 当我们创建管道时,是由操作系统打开的,其系统调用为:通过pipe创建的管道叫做匿名管道
#include <unistd.h>/* On all other architectures */
int pipe(int pipefd[2]);RETURN VALUEOn success, zero is returned. On error, -1 is returned, errno is set appropriately, and pipefd is left unchanged.
当我们创建一个管道文件,默认是以读写方式打开的,会返回两个文件描述符分别对应写和读,3对应写读,4对应写。
#include <iostream>
#include <unistd.h>int main()
{int pipes[2] = {0};int n = pipe(pipes);if(n < 0) std::cerr << "pipe error" << std::endl;std::cout << "pipes[0]:" << pipes[0] << std::endl;std::cout << "pipes[1]:" << pipes[1] << std::endl;return 0;
}
0x2.构建通信信道
匿名管道用来父子进程之间的通信,现在有了管道,下一步就是创建子进程,并构建通信信道。
创建子进程后,子进程会拷贝父进程的pcb和文件描述符表,所以此时子进程也拿到管道的访问权了。
接着便是构建通信信道,而在父子进程通信遵从的是单向通信。即一个写,一个读。所以我们要关闭父子进程不需要的端口。
自此,我们就构建好了一个通信信道,此时父子进程就可以进程通信。但是在这个过程中,父进程只能写,而子进程只能读,不能被修改。
0x3.测试父子进程间的通信
要求:创建父子进程间单向通信,父进程读,子进程写。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>void ChildWrite(int wfd)
{char buffer[1024] = {0};int cnt = 0;while (true){snprintf(buffer, sizeof(buffer), "i am child: pid->%d, cnt->%d", getpid(), cnt++);write(wfd, buffer, strlen(buffer));sleep(1);}
}void FatherRead(int rfd)
{char buffer[1024] = {0};while (true){ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;std::cout << "child say:" << buffer << std::endl;}}
}int main()
{// 1.打开管道文件,一般fds[0]:read, fds[1]:writeint fds[2] = {0};int n = pipe(fds);// std::cout << "fds[0]:" << fds[0] << std::endl;// std::cout << "fds[1]:" << fds[1] << std::endl;if (n < 0){std::cerr << "pipe error" << std::endl;return 1;}// 2.创建子进程并构建单向通信信道->father r, child wpid_t id = fork();if (id == 0){// childclose(fds[0]);// operateChildWrite(fds[1]);close(fds[1]);exit(1);}// fatherclose(fds[1]);// operateFatherRead(fds[0]);waitpid(id, nullptr, 0);close(fds[0]);return 0;
}
说明:构建通信信道之后,我们让子进程写,父进程读。子进程每个一秒向管道里写内容,而父进程一直读管道。我们看结果:
虽然我们一直在读,但是因为写有间隔,结果依旧是一秒打印一条消息。为什么读与写的时间相匹配?
当管道里面没有内容的时候,再读文件内容的话,此时读的进程就会阻塞住。直到管道内有内容。 这就是管道的同步机制。
0x4.管道的5种特性与4种通信情况
5种特性:
1、匿名管道只能用于具有亲缘关系的进程间的通信(常用于父子进程)
2、管道文件自带同步机制
正如上面的测试代码,读和写的频率是不同,当写的慢,读的快时,读就会阻塞住;当写的快,读的慢时,管道文件满了,写端就会阻塞住。
当我们让写的快,读的慢时:
说明: 写的快,一下子就有可能将管道文件写满,也有可能到了读的时候,此时就只能等待读端读取管道文件内容,否则无法进行下一次写入。所以读一次管道文件就会带出很多的内容。
3、管道是面向字节流的
- 管道文件是面向字节流的,这意味着管道不关心数据的结构和类型,只负责将字节从一个进程传递到另一个进程
- 写入管道的数据被视为连续的字节流,没有明确的消息边界,也就是说,写入与读取无关,读取的结果可能与写入有偏差,取决于我想怎么读。
4、管道是单向通信的
数据只能从写端传递到读端。管道是一种特殊的半双工,因为其写端和读端不能交换。
任意时刻,一个发,一个收——半双工
任意时刻,可以同时收发——全双工
5、管道文件的声明周期是随进程的。进程结束,管道就会被OS回收。
4种通信情况:
1、写慢,读快——读端就要阻塞等待写端写入
2、写快,读慢——当管道文件被写满了,此时写端就要阻塞等待读端读取文件内容,之后才能继续写入
3、写关,继续读——read就会读到返回值为0,表示文件结尾
void ChildWrite(int wfd)
{char buffer[1024] = {0};int cnt = 10;while (cnt){snprintf(buffer, sizeof(buffer), "i am child: pid->%d, cnt->%d", getpid(), cnt--);write(wfd, buffer, strlen(buffer));sleep(1);}
}void FatherRead(int rfd)
{char buffer[1024] = {0};while (true){// sleep(5);ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;std::cout << "child say:" << buffer << std::endl;}else if(n == 0){std::cout << "n:" << n << std::endl;sleep(1);}}
}
说明:我们只写十次,随后让子进程关闭写端,读端此时在读时就会拿到返回值0,表示读到了文件结尾。
4.读关,写继续
写端写入没有任何意义,数据不会被其他进程所使用,白白占用了资源,而OS系统就是做资源管理的,所以不会让别人浪费资源,所以操作系统会杀死进程,通过发送异常信号SIGPIPE
...void FatherRead(int rfd)
{char buffer[1024] = {0};int cnt = 5;while (cnt--){// sleep(5);ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;std::cout << "child say:" << buffer << std::endl;}else if(n == 0){std::cout << "n:" << n << std::endl;sleep(1);}}
}...// 关闭读端close(fds[0]);int status = 0;waitpid(id, &status, 0);std::cout << "exit code->" << ((status >> 8) & 0xFF) << "exit signal->" << (status & 0x7F) << std::endl;
说明:一直写,而只读5次,接着关闭读端,此时根据我们所说,操作系统会通过信号杀死进程。而此时我们再等待子进程时,获取其退出状态。我们前面还说过,当子进程接收到信号异常结束后,此时的退出码是无效的——0,而退出信号就应该对应——SIGPIPE
五.进程池
了解了上面的匿名管道实现父子进程间单向通信之后,我们通过该机制设计一个进程池——提前创建出多个子进程,等到要使用子进程执行任务时,直接从进程池中找进城去执行,而不用在通过fork创建子进程,从而减少进程创建和销毁的开销,提高系统资源利用率和任务处理效率。
其实就类似于内存池的概念,为了避免频繁的在堆上申请空间,可以先开辟一大块内存,再想申请空间时,就直接从内存池拿!
1.先描述在组织
进程池本质上是为了完成我们给出的任务,而任务我们要怎么给进程呢?通过管道!
所以我们管理进程池本质上就是在管理一个一个的通信信道。而管理就得先描述在组织!
组织信道我们额外创建一个类并用数组的方式保存所有的信道。
而对于进程池来说,他就是对所有的信道进行任务发送,所以它的成员就是所有的信道!
// 先描述——通信信道
class Channel
{
public:Channel(){}~Channel(){}
private:
};// 在组织——管理信道
class ChannelManager
{
public:ChannelManager(){}~ChannelManager(){}
private:std::vector<Channel> _channels;
};// 进程池
class ProcessPool
{
public:ProcessPool(){}~ProcessPool(){}
private:ChannelManager _cm;
};
2.初始化进程池
初始化进程池其实就是创建通信信道,并将所有的通信信道与对应的子进程关联起来。我们最终想要达到的目的应该是父进程可以通过多个不同的文件描述发送任务,即写任务到管道文件中,让对应的子进程读取任务,然后执行!
我们默认创建5个子进程的进程池:
// 先描述——通信信道
class Channel
{
public:Channel(int wfd, int id):_wfd(wfd),_id(id){_ref = "channel-" + std::to_string(_wfd) + "-" + std::to_string(_id);}~Channel(){}
private:int _wfd; // 信道号int _id; // 对应子进程std::string _ref; // 信道描述信息
};// 在组织——管理信道
class ChannelManager
{
public:ChannelManager(){}~ChannelManager(){}void manage(int wrd, int id){_channels.emplace_back(wrd, id);}private:std::vector<Channel> _channels;
};const int DefaultProcessPoolCount = 5;// 进程池
class ProcessPool
{
public:ProcessPool(int count = DefaultProcessPoolCount ):_process_count(count){}~ProcessPool(){}bool InitPool(){for (int i = 0; i < _process_count; ++i) // 循环创建多个子进程与管道文件的对应关系{// 1.打开管道文件int pipefd[2] = {0};int n = pipe(pipefd);if (n < 0){std::cerr << "pipe error" << std::endl;return -1;}// 2.创建子进程,并构建单向通信信道 -> fathrer write, child readpid_t id = fork();if (id < 0){std::cerr << "fork errror" << std::endl;}else if (id == 0){// childclose(pipefd[1]);Work(pipefd[0]); // 执行任务close(pipefd[0]);exit(0);}else{// fatherclose(pipefd[0]);// 3.将创建出来的信道与子进程关联_cm.manage(pipefd[1], id);}}
// 测试代码
#ifdef DEBUG_cm.debug();sleep(10);
#endifreturn true;}private:ChannelManager _cm;int _process_count; // 进程池中的子进程数
};
3.任务管理器
我们想让子进程根据我们所写入到管道文件中的任务码来执行对应的任务。我们可以借助哈希表来存储对应的任务码和任务函数。但是哈希表无法存储函数,所以我们要用包装器对函数进行包装,使之成为统一的类型,以被哈希表存储。
我们分别设计以下方法,用来进行任务的管理等操作:
- 注册任务:用来将对应的任务码和对应的函数存储起来。
- 获取任务码:我们这里采用随机值获取任务码来模拟发送不同任务的场景
- 执行任务:执行任务的逻辑其实就是访问对应任务码的value。
// task.hpp#pragma once
#include <iostream>
#include <string>
#include <ctime>
#include <functional>
#include <unordered_map>// task for test
void PrintLog()
{std::cout << "task for 打印日志" << std::endl;
}void DownLoad()
{std::cout << "task for 下载" << std::endl;
}void UPLoad()
{std::cout << "tas for 上传" << std::endl;
}class TaskManager
{
public:TaskManager() {srand(time(nullptr));}~TaskManager() {}void Register(int code, std::function<void()> task){_taskCatalog[code] = task;}int taskcode(){int r = rand();return r % _taskCatalog.size();}void Exectue(int taskcode){_taskCatalog[taskcode]();}private:std::unordered_map<int, std::function<void()>> _taskCatalog;
};
4.选择与执行任务
有了任务管理器之后,我们的进程池还应该有一个任务管理器成员,用来选择任务执行任务等操作。另外我们在初始化进程池时还应该注册对应的任务!!!
class ProcessPool
{
public:ProcessPool(int n = defaultPoolCount) :_process_count(n){// 注册任务_tm.Register(0, PrintLog);_tm.Register(1, DownLoad);_tm.Register(2, UPLoad);}...
private:ChannelManager _cm;TaskManager _tm;int _process_count;
};
选择任务分为两部分,一是选择任务,二是选择子进程
0x1.选择任务
选择任务非常简单,我们只需要调用对应的tm的获取任务码的方法即可。
0x2.选择子进程
有了任务之后,我们还得选择让那个子进程去执行。因为我们有多个子进程,所以我们肯定得平衡每个子进程执行任务的次数,不能让某个子进程忙死,其他子进程闲死。
我们可以采用轮询、随机数、channel负载指标等来控制。
这里我们采用轮询的方式来选择。
轮询即第一次使用第一个子进程,第二次使用第二个,一次类推。我们只需要使用一个int变量来记录当前要使用的子进程即可,为了避免该变量越界,所以我们还要采取取模的方式!
0x3.发布任务
选择好任务和子进程后,我们接下来就得发布任务了。而发布任务其实就是将对应的任务码写入到对应通信信道中。
// channel...void NotifyTask(int taskcode){int n = write(_wfd, &taskcode, sizeof(taskcode));(void)n; // 这里定义了一个没有使用的变量,这样是为了避免warnning}...// channelmanager...Channel& Select(){auto& c = _channels[_next];_next++;_next %= _channels.size();return c;}...
private:std::vector<channel> channels;int _next;// processpool...// 选择任务,选择信道void RunTask(){// 1.选择任务int taskcode = _tm.taskcode();std::cout << "任务已选择..." << std::endl;// 2.选择信道auto& channel = _cm.Select();std::cout << "信道已选择--->" << channel.Ref() << std::endl;// 3.发布任务channel.NotifyTask(taskcode);std::cout << "任务已通知..." << std::endl;}...
0x4.执行任务
当我们在发布任务之前,所有没有执行任务的子进程都应该处于read的阻塞等待中。因为我们一直没有写,相当于写的慢读的快。
当我们一写入,子进程就可以从对应的管道中读取任务码,接下来就是执行任务了。而执行任务我们已经在任务管理器中定义了,所以这里只需要获取任务码,调用对应的执行函数就行了。
// processpool...// 执行任务void Work(int rfd){int code = 0;while (true){ssize_t n = read(rfd, &code, sizeof(code));// std::cout << "n:" << n << std::endl;if (n > 0){if(n != sizeof(code)) // 如果读到的不是一个整数表明非法任务,重新等待continue;std::cout << "开始执行任务" << std::endl;_tm.Exectue(code);}else if (n == 0){std::cout << "读到文件末尾" << std::endl;break;}else{std::cerr << "读取错误" << std::endl;break;}}}...
5.进程池的销毁
我们销毁进程池其实就是销毁所有的信道以及回收对应的子进程。而销毁信道我们之前说过,如果一个管道只剩写端,没有读端,此时是没有意义的,OS会将该管道关闭,所以关闭信道其实只需要让子进程关闭对应的读端即可。
回收子进程我们只需要使用waitpid系统调用即可。
// channel...void closewfd(){int n = close(_wfd);(void)n;}void waitid(){int n = waitpid(_id, nullptr, 0);if(n < 0) std::cerr<<"wiat error" << std::endl;}...// channelmanager...void DestroyChannel(){for(auto& channel : _channels){channel.closewfd();std::cout << channel.Ref() << "closed !" << std::endl;}}void RecycleProcess(){for(auto& channel : _channels){channel.waitid();std::cout << channel.Ref() << "recycled !" << std::endl;}}...// processpool...// 销毁进程池void Destroy(){// 1.关闭所有信道_cm.DestroyChannel();// 2.回收子进程_cm.RecycleProcess();}...
自此,我们就实现了基于匿名管道的进程池。
相关文章:
【Linux】进程间通信、匿名管道、进程池
一.什么是通信 进程间通信(Inter-Process Communication,IPC),是指在操作系统中,不同进程之间进行数据交换和同步的机制。由于每个进程通常拥有独立的内存空间,进程间无法直接访问对方的内存,因此需要通过特定的机制来实现通信和…...
【Block总结】PlainUSR的局部注意力,即插即用|ACCV2024
论文信息 标题: PlainUSR: Chasing Faster ConvNet for Efficient Super-Resolution作者: Yan Wang, Yusen Li, Gang Wang, Xiaoguang Liu发表时间: 2024年会议/期刊: 亚洲计算机视觉会议(ACCV 2024)研究背景: 超分辨率(Super-Resolution, S…...
35信号和槽_信号槽小结
Qt 信号槽 1.信号槽是啥~~ 尤其是和 Linux 中的信号进行了对比(三要素) 1) 信号源 2) 信号的类型 3)信号的处理方式 2.信号槽 使用 connect 3.如何查阅文档. 一个控件,内置了哪些信号,信号都是何时触发 一…...
现代复古电影海报品牌徽标设计衬线英文字体安装包 Thick – Retro Vintage Cinematic Font
Thick 是一种大胆的复古字体,专为有影响力的标题和怀旧的视觉效果而设计。其厚实的字体、复古魅力和电影风格使其成为电影海报、产品标签、活动品牌和编辑设计的理想选择。无论您是在引导电影的黄金时代,还是在现代布局中注入复古活力,Thick …...
低代码开发平台:飞帆画 echarts 柱状图
https://fvi.cn/711 柱状图这个控件是由折线图的控件改过来的,在配置中,单选框选择柱状图就行了。...
Linux中C++ gdb调试命令
编译可执行文件需要带上-g选项参数 输入回车则重复执行上一次命令; 进入gdb: gdb 程序名运行gdb命令: r打断点命令: b 行号查看断点命令: i b打印变量命令: p 变量名持续查看变量命令: d…...
Python精进系列:从 __name__ 开始了解 python 常见内置变量
目录 引言一、__name__是什么?案例1:直接运行模块案例2:模块被导入 二、__name__的主要用途(一)区分主程序和导入模块案例3:测试代码隔离(二)动态导入模块案例4:根据环境…...
Nacos 服务发现的核心模型有哪些?Service, Instance, Cluster 之间的关系是什么?
Nacos 服务发现的核心模型 Nacos 服务发现的核心数据模型主要围绕以下几个关键概念构建,它们共同构成了服务注册与发现的基础: Namespace (命名空间): 用途: 用于进行环境隔离。比如,你可以为开发环境 (dev)、测试环境 (test) 和生产环境 (p…...
Java程序设计第1章:概述
一、Hello World 1.代码: public class HelloWorld {public static void main(String[] args){System.out.println("Hello World!");} } 2.运行结果: Hello World! 二、输出姓名、学号、班级 1.题目: 编写一个Application&a…...
C++开发工具全景指南
专业编译与调试工具深度解析 2025年4月 编译器套件 GNU Compiler Collection (GCC) GNU编译器套件是自由软件基金会开发的跨平台编译器系统,支持C、C、Objective-C、Fortran、Ada等多种编程语言。作为Linux系统的标准编译器,GCC以其强大的优化能力和…...
Java的Selenium的特殊元素操作与定位之iframe切换
iframe切换 四种切换方式: driver.switchTo().frame(index);driver.switchTo().frame(id);driver.switchTo().frame(name);driver.switchTo().frame(WebElement); 切换之后,回到默认内容页面(否则会找不到元素 driver.switchTo().defaultContent(); //iframe处…...
AI比人脑更强,因为被植入思维模型【42】思维投影思维模型
giszz的理解:本质和外在。我们的行为举止,都是我们的内心的表现。从外边可以看内心,从内心可以判断外在。曾国藩有7个识人的方法,大部分的人在他的面前如同没穿衣服一样。对于我们自身的启迪,我认为有四点&…...
7-12 最长对称子串(PTA)
对给定的字符串,本题要求你输出最长对称子串的长度。例如,给定Is PAT&TAP symmetric?,最长对称子串为s PAT&TAP s,于是你应该输出11。 输入格式: 输入在一行中给出长度不超过1000的非空字符串。 输出格式&…...
嵌入式AI的本地化部署的好处
嵌入式AI本地化处理(即边缘计算)的核心优势在于将AI算力下沉至设备端,直接处理数据而非依赖云端,这种模式在多个维度上展现出显著价值: 一、数据隐私与安全性提升 1. 敏感数据本地存储 金融、医疗等涉及隐私的行业…...
0基础 | 硬件 | 电源系统 一
降压电路LDO 几乎所有LDO都是基于此拓扑结构 图 拓扑结构 LDO属于线性电源,通过控制开关管的导通程度实现稳压,输出纹波小,无开关噪声 线性电源,IoutIin,发热功率P电压差△U*电流I,转换效率Vo/Vi LDO不适…...
LeetCode详解之如何一步步优化到最佳解法:20. 有效的括号
LeetCode详解系列的总目录(持续更新中): LeetCode详解之如何一步步优化到最佳解法:前100题目录(更新中...)-CSDN博客 LeetCode详解系列的上一题链接: LeetCode详解之如何一步步优化到最佳解法…...
LeetCode18四数之和
代码来源:代码随想录 /*** Return an array of arrays of size *returnSize.* The sizes of the arrays are returned as *returnColumnSizes array.* Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().*/ int com…...
《K230 从熟悉到...》无线网络
《K230 从熟悉到...》无线网络 STA模式 《庐山派 K230 从熟悉到...》无线网络 无线网络中通常是STA(Station,站点)和AP(Access Point,无线接入点)。 STA(站点) 定义:STA…...
去中心化指数(链上ETF)
去中心化指数(链上ETF) 核心概念 去中心化指数: 类似传统金融的ETF(交易所交易基金),通过一篮子代币分散投资风险,无需主动管理。 核心价值:降低研究成本、分散风险、自动化资产…...
LeeCode题库第1695题
项目场景: 给你一个正整数数组 nums ,请你从中删除一个含有 若干不同元素 的子数组。删除子数组的 得分 就是子数组各元素之 和 。 返回 只删除一个 子数组可获得的 最大得分 。 如果数组 b 是数组 a 的一个连续子序列,即如果它等于 a[l],…...
【LeetCode 热题100】23:合并 K 个升序链表(详细解析)(Go语言版)
🚀 LeetCode 热题 23:合并 K 个升序链表(详细解析) 📌 题目描述 LeetCode 23. Merge k Sorted Lists 给你一个链表数组,每个链表都已经按升序排列。 请你将所有链表合并到一个升序链表中,返回合…...
LeetCode hot 100—删除链表的倒数第N个节点
题目 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。 示例 示例 1: 输入:head [1,2,3,4,5], n 2 输出:[1,2,3,5]示例 2: 输入:head [1], n 1 输出:[]示例 3&…...
超级科学软件实验室(中国) : Super Scientific Software Laboratory (SSSLab)
Super Scientific Software Laboratory (SSSLab) gitee 官网...
2025大唐杯仿真1——车联网
车联网 V2N是指车辆与网络 Uu接口是用户设备(UE)与基站之间的通信接口,用于终端和基站之间的通信 Uu接口可用的是N41频段,归属中国移动 车辆间交互是V2V,频段是PCS PC5接口是一种用于设备间直接通信(D2D…...
云资源合规基线:确保云环境安全与合规的完整指南
1. 引言 随着越来越多的企业将其IT基础设施迁移到云端,确保云资源的安全性和合规性变得至关重要。云资源合规基线是一套最佳实践和标准,旨在帮助组织维护安全、高效且符合法规要求的云环境。本文将深入探讨云资源合规基线的各个方面,为IT管理者和安全专业人士提供全面的指导。…...
1.0 软件测试全流程解析:从计划到总结的完整指南
软件测试全流程解析:从计划到总结的完整指南 摘要 本文档详细介绍了软件测试的完整流程,包括测试计划、测试设计、测试执行、测试报告和测试总结等主要阶段。每个阶段都从目标、主要工作、输出物和注意事项等方面进行了详细说明。通过本文档࿰…...
@reduxjs/toolkit 报错,解决
项目场景: 使用redux存储状态,写一个reducer 问题描述 报错:Uncaught Error: A case reducer on a non-draftable value must not return undefined import { createSlice } from "reduxjs/toolkit"; //错误写法 const counterS…...
C++蓝桥杯实训篇(二)
片头 嗨咯~小伙伴们!今天我们来一起学习算法和贪心思维,准备好了吗?咱们开始咯! 第1题 数位排序 对于这道题,我们需要自己写一个排序算法,也就是自定义排序,按照数位从小到大进行排序。 举一…...
YY forget password
YY forget password 老早以前的语音工具,游戏团队协作工具...
Kafka 如何解决消息堆积问题?
Kafka 的消息堆积问题是实际生产中经常遇到的情况,尤其在高并发、大流量、消费者故障或处理速度慢的情况下,非常容易出现。 下面我从诊断 解决方案 实战技巧三步帮你梳理清楚: 🔍 一、先判断:是否真的“堆积”&…...
如何通过优化HMI设计大幅提升产品竞争力?
一、HMI设计的重要性与竞争力提升 HMI(人机交互界面)设计在现代产品开发中扮演着至关重要的角色。良好的HMI设计不仅能够提升用户体验,还能显著增强产品的竞争力。在功能趋同的市场环境中,用户体验成为产品竞争的关键。HMI设计通…...
2025大唐杯仿真4——信令流程
Preamble请求...
MyBatis Plus 在 ZKmall开源商城持久层的优化实践
ZKmall开源商城作为基于 Spring Cloud 的高性能电商平台,其持久层通过 MyBatis Plus 实现了多项深度优化,涵盖分库分表、缓存策略、分页性能、多租户隔离等核心场景。以下是具体实践总结: 一、分库分表与插件集成优化 1. 分库分表策略 Sh…...
Qt多线程从基础到性能优化
一、为什么需要多线程开发 现代应用程序的性能需求 CPU多核架构的有效利用 复杂任务的解耦与响应式界面保持 二、Qt线程创建四大方式 1. 继承QThread重写run() class WorkerThread : public QThread {void run() override {// 耗时操作qDebug() << "Thread ID…...
Spring常见问题复习
############Spring############# Bean的生命周期是什么? BeanFactory和FactoryBean的区别? ApplicationContext和BeanFactory的区别? BeanFactoryAware注解,还有什么其它的Aware注解 BeanFactoryAware方法和Bean注解的方法执行顺…...
股票日数据使用_未复权日数据生成前复权日周月季年数据
目录 前置: 准备 代码:数据库交互部分 代码:生成前复权 日、周、月、季、年数据 前置: 1 未复权日数据获取,请查看 https://blog.csdn.net/m0_37967652/article/details/146435589 数据库使用PostgreSQL。更新日…...
【C++】从零实现Json-Rpc框架(2)
目录 JsonCpp库 1.1- Json数据格式 1.2 - JsonCpp介绍 • 序列化接口 • 反序列化接口 1.3 - Json序列化实践 JsonCpp使用 Muduo库 2.1 - Muduo库是什么 2.2 - Muduo库常见接口介绍 TcpServer类基础介绍 EventLoop类基础介绍 TcpConnection类基础介绍 TcpClient…...
JVM虚拟机篇(二):深入剖析Java与元空间(MetaSpace)
这里写目录标题 JVM虚拟机篇(二):深入剖析Java与元空间(MetaSpace)一、引言二、全面认识Java2.1 Java的起源与发展历程2.2 Java的特性2.2.1 简单性2.2.2 面向对象2.2.3 平台无关性2.2.4 健壮性2.2.5 安全性2.2.6 多线程…...
NDK开发:音视频处理基础
音视频处理基础 一、音视频基础 1.1 音视频基本概念 视频编码格式 H.264/AVCH.265/HEVCVP8/VP9AV1音频编码格式 AACMP3PCMOPUS封装格式 MP4FLVMKVTS1.2 音视频处理流程 视频处理流程 采集(Camera/Screen)预处理(美颜/滤镜)编码(H.264/H.265)封装传输/存储音频处理流程 …...
【数字电路】第一章 数制和码制
一、数码的基本概念 1.数制 2.码制 二、几种常用的数制 三、不同数制间的转换 八进制和十六进制间通常不直接进行转换,而是先转换成二进制或十进制然后再进行转换。 1.任意进制→十进制(N—十转换) 2.十进制→任意进制(十—N转换…...
软件工程面试题(二十九)
1、Internet的最顶级的商业域名叫什么? 答: .com 2、GC是什么,为什么要使用它? 垃圾回收 (garbage collection, GC) 一个跟踪过程,它传递性地跟踪指向当前使用的对象的所有指针,以便找到可以引用的所有对象,然后重新使用在此跟踪过程中未找到的任何堆内存。公共语言运行…...
6.第二阶段x64游戏实战-分析人物状态
免责声明:内容仅供学习参考,请合法利用知识,禁止进行违法犯罪活动! 本次游戏没法给 内容参考于:微尘网络安全 上一个内容:5.第二阶段x64游戏实战-动态模块地址 人物状态是与角色相关的,如果…...
Synopsys:设计对象
相关阅读 Synopsyshttps://blog.csdn.net/weixin_45791458/category_12812219.html?spm1001.2014.3001.5482 对于Synopsys的EDA工具(如Design Compiler、PrimeTime、IC Compiler)等,设计对象(Design Objects)是组成整个设计的抽象表示&…...
Redis数据结构之Hash
目录 1.概述2.常见操作2.1 H(M)SET/H(M)GET2.2 HGETALL2.3 HDEL2.4 HLEN2.5 HEXISTS2.6 HKEYS/HVALS2.7 HINCRBY2.8 HSETNX 3.总结 1.概述 Hash是一个String类型的field(字段)和value(值)的映射表,而且value是一个键值对集合,类似Map<String, Map<…...
【VUE】RuoYi-Vue3项目结构的分析
【VUE】RuoYi-Vue3项目结构的分析 1. 项目地址2. RuoYi-Vue3项目结构2.1 整体结构2.2 package.json2.2.1 🧾 基本信息2.2.2 🔧 脚本命令(scripts)2.2.3 🌍 仓库信息2.2.4 📦 项目依赖(dependenc…...
libreoffice-help-common` 的版本(`24.8.5`)与官方源要求的版本(`24.2.7`)不一致
出现此错误的原因主要是软件包依赖冲突,具体分析如下: ### 主要原因 1. **软件源版本不匹配(国内和官方服务器版本有差距) 系统中可能启用了第三方软件源(如 PPA 或 backports 源),导致 lib…...
5.数据手册解读——共模电感
目录 1 共模电感的工作原理 2 核心参数解读 2.1 电气参数 2.2 阻抗特性 共模电感(Common mode Choke),也叫共模扼流圈,是在一个闭合磁环上对称绕制方向相反、匝数相同的线圈。理想的共模扼流圈对L(或N)与E之间的共模干扰具有抑…...
easy-poi 一对多导出
1. 需求: 某一列上下两行单元格A,B值一样且这两个单元格, 前面所有列对应单元格值一样的话, 就对A,B 两个单元格进行纵向合并单元格 1. 核心思路: 先对数据集的国家,省份,城市...... id 身份证进行排序…...
用C语言控制键盘上的方向键
各位同学,大家好!相信大家在学习C语言的过程中,都和我一样,经常使用scanf函数来接受字符,数字,这些标准输入信息,来实现自己设计的程序效果。 而我突然有一天(对就是今天)…...
第3课:状态管理与事件处理
第3课:状态管理与事件处理 学习目标 掌握useState Hook的使用理解组件事件处理机制实现表单输入与状态绑定完成任务添加功能原型 一、useState基础 1. 创建第一个状态 新建src/Counter.js: import { useState } from react;function Counter() {co…...