【Linux系统编程】第三十四弹---使用匿名管道构建简易Linux进程池

个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】

目录

1、引言

2、进程池的基本概念

3、管道在进程池中的应用

4、进程池的实现

4.1、master类定义

4.2、测试信道

4.3、通过channle控制子进程 

4.3.1、普通版本

4.3.2、重定向版本

4.4、回收管道和子进程 

4.5、修复bug

5、进程池完整代码

5.1、makefile

5.2、Task.hpp

5.3、ProcessPool.cc


1、引言

在现代的软件开发中,处理大量并发任务是一项常见的需求。进程池作为一种有效的并发处理模型,能够预先创建多个进程并管理它们,以响应不断到来的任务。本文将介绍如何使用C++和Linux系统下的管道(Pipes)机制来构建一个简易的进程池

2、进程池的基本概念

进程池是一种技术,它预先创建并维护一定数量的进程,这些进程在需要时执行特定的任务。进程池减少了进程创建和销毁的开销,提高了系统资源的利用率,并简化了并发任务的管理

3、管道在进程池中的应用

管道是Linux系统中用于进程间通信(IPC)的一种简单机制。它允许一个进程(写端)将数据写入管道,并由另一个进程(读端)读取。在本例中,我们将使用管道来从主进程向子进程发送任务命令。

4、进程池的实现

会用到的头文件

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctime>
#include <cstdlib>

进程池基本结构

4.1、master类定义

// master信道
class Channel
{
public:// 构造函数Channel(int wfd, pid_t id, const std::string &name): _wfd(wfd), _subprocessid(id), _name(name){}// 获取成员变量函数int GetWfd() const { return _wfd; }pid_t GetProcessId() const { return _subprocessid; }std::string GetName() const { return _name; }~Channel(){}private:int _wfd;            // 写端fdpid_t _subprocessid; // 子进程pidstd::string _name;   // 信道名字
};

4.2、测试信道

1、创建管道

2、创建子进程

3、构建一个channel名称

4、测试是否创建成功

void work(int fd)
{while(true){sleep(1);}
}
int main(int argc,char* argv[])
{// 通过命令行传参创建几个子进程,不传个数直接报错if(argc != 2){std::cerr << "Usage: " << argv[0] << "processnum" << std::endl;return 1;}int num = std::stoi(argv[1]);std::vector<Channel> channels;// 1.创建信道和子进程for(int i = 0;i < num;i++){// 1.创建管道int pipefd[2] = {0};int n = pipe(pipefd);// n < 0创建失败if(n < 0) exit(0);// 2.创建子进程pid_t id = fork();// child -- readif(id == 0){// 关闭写端close(pipefd[1]);//work(pipefd[0]);close(pipefd[0]);exit(0);}// 3.构建一个channel名称std::string channel_name = "channel-" + std::to_string(i);// father -- writeclose(pipefd[0]);channels.push_back(Channel(pipefd[1],id,channel_name));}// for testfor(auto& channel : channels){std::cout << "====================================" << std::endl;std::cout << "channel_name->" << channel.GetName() << std::endl;std::cout << "channel_wfd->" << channel.GetWfd() << std::endl;std::cout << "channel_id->" << channel.GetProcessId() << std::endl;}sleep(100);return 0;
}

将创建信道和子进程封装成函数

// 创建信道和子进程
void CreateChannelAndSub(int num, std::vector<Channel> *channels)
{// Bugfor (int i = 0; i < num; i++){// 1.创建管道int pipefd[2] = {0};int n = pipe(pipefd);// n < 0创建失败if (n < 0)exit(0);// 2.创建子进程pid_t id = fork();// child -- readif (id == 0){// 关闭写端close(pipefd[1]);//work(pipefd[0]);close(pipefd[0]);exit(0);}// 3.构建一个channel名称std::string channel_name = "channel-" + std::to_string(i);// father -- writeclose(pipefd[0]);channels->push_back(Channel(pipefd[1], id, channel_name));}
}

4.3、通过channle控制子进程 

4.3.1、普通版本

1、选择一个任务

2、选择一个信道和进程

3、发送任务

4、测试

1、创建一些任务(此处使用函数指针)

// 任务个数
#define TaskNum 3typedef void (*task_t)();//函数指针重命名 void Print()
{std::cout << "I am a Print task" << std::endl;
}
void DownLoad()
{std::cout << "I am a Download task" << std::endl;
}
void Flush()
{std::cout << "I am a Flush task" << std::endl;
}// 创建任务数组
task_t tasks[TaskNum]; 

2、调用任务的相关函数

// 加载任务
void LoadTask()
{srand(time(nullptr) ^ getpid() ^1777);tasks[0] = Print;tasks[1] = DownLoad;tasks[2] = Flush;
}// 通过数组下标执行某一条任务
void ExecuteTask(int number)
{if(number < 0 || number > 2) return;tasks[number]();
}// 通过随机数选择任务编号
int SelectTask()
{return rand() % TaskNum;
}

3、选择信道函数(轮询方式)

从第一个信道开始,依次进行使用。

// 0 1 2 channelnum
int NextChannel(int channelnum)
{static int next = 0;int channel = next;next++;next %= channelnum;return channel;
}

4、发送任务函数

发送任务实质是向写端写入执行任务的编号。

void SendTaskCommand(const Channel &channel, int taskcommand)
{write(channel.GetWfd(), &taskcommand, sizeof(taskcommand));
}

5、控制子进程主体函数

// 控制一次子进程
void ctrlProcessOnce(std::vector<Channel> &channels)
{sleep(1);// a.选择一个任务int taskcommand = SelectTask();// b.选择一个信道和进程int channel_index = NextChannel(channels.size());// c.发送任务SendTaskCommand(channels[channel_index], taskcommand);std::cout << std::endl;std::cout << "taskcommand: " << taskcommand << " channel: "<< channels[channel_index].GetName() << " sub process: " << channels[channel_index].GetProcessId() << std::endl;
}
// 通过times参数控制执行多少次,不传参则一直执行
void ctrlProcess(std::vector<Channel> &channels, int times = -1)
{if (times > 0){while (times--){ctrlProcessOnce(channels);}}else{while (true){ctrlProcessOnce(channels);}}
}

6、主函数

int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << "processnum" << std::endl;return 1;}int num = std::stoi(argv[1]);// 加载任务LoadTask();std::vector<Channel> channels;// 1.创建信道和子进程CreateChannelAndSub(num, &channels);// 2.通过channle控制子进程ctrlProcess(channels);return 0;
}

4.3.2、重定向版本

 我们可以将管道的读端,重定向到标准输入,然后从标准输入读取数据。

work()函数

void work()
{while (true){int command = 0;// 从标准输入读数据int n = read(0, &command, sizeof(command));if (n == sizeof(int)){std::cout << "pid is : " << getpid() << " handler task" << std::endl;ExecuteTask(command);}else if (n == 0){std::cout << "sub process : " << getpid() << " quit" << std::endl;break;}}
}

创建信道和子进程 

// 创建信道和子进程(回调方式)
void CreateChannelAndSub(int num, std::vector<Channel> *channels,task_t task)
{// Bugfor (int i = 0; i < num; i++){// 1.创建管道int pipefd[2] = {0};int n = pipe(pipefd);// n < 0创建失败if (n < 0)exit(0);// 2.创建子进程pid_t id = fork();// child -- readif (id == 0){// 关闭写端close(pipefd[1]);dup2(pipefd[0], 0); // 将管道的读端,重定向到标准输入task();close(pipefd[0]);exit(0);}// 3.构建一个channel名称std::string channel_name = "channel-" + std::to_string(i);// father -- writeclose(pipefd[0]);channels->push_back(Channel(pipefd[1], id, channel_name));}
}

主函数 

int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << "processnum" << std::endl;return 1;}int num = std::stoi(argv[1]);// 加载任务LoadTask();std::vector<Channel> channels;// 1.创建信道和子进程,回调版本,调用什么函数,传什么函数名CreateChannelAndSub(num, &channels,work);// 2.通过channle控制子进程,执行5次ctrlProcess(channels,5);// 3. 回收管道和子进程. a. 关闭所有的写端 b. 回收子进程CleanUpChannel(channels);return 0;
}

 可以实现一样的效果,但是我们可以将任务单独放在一个文件中,想要调用哪个任务,实现一个任务函数,传这个任务函数名即可。

4.4、回收管道和子进程 

1、关闭所有的写端

2、回收子进程

1、关闭所有写端

在结构体内部实现关闭写端函数即可,即关闭写文件描述符即可。

void CloseChannel()
{close(_wfd);
}

2、回收子进程

通过父进程把子进程回收即可,即waitpid()。

void Wait()
{pid_t rid = waitpid(_subprocessid, nullptr, 0);if (rid > 0){std::cout << "wait " << rid << " success" << std::endl;}
}

3、清理信道函数

void CleanUpChannel(std::vector<Channel> &channels)
{for (auto &channel : channels){channel.CloseChannel();}for (auto &channel : channels){channel.Wait();}
}

4、主函数

int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << "processnum" << std::endl;return 1;}int num = std::stoi(argv[1]);// 加载任务LoadTask();std::vector<Channel> channels;// 1.创建信道和子进程CreateChannelAndSub(num, &channels);// 2.通过channle控制子进程,执行5次ctrlProcess(channels,5);// 3. 回收管道和子进程. a. 关闭所有的写端 b. 回收子进程CleanUpChannel(channels);return 0;
}

对上面的清理函数做一个小小的修改!!!

 关闭写端之后就回收子进程。

void CleanUpChannel(std::vector<Channel> &channels)
{for (auto &channel : channels){channel.CloseChannel();channel.Wait();}
}

现象是执行完5次之后,子进程阻塞了,没有被关闭。 

为什么会出现上面的情况,子进程阻塞了呢???

因为关闭写端文件描述符,只是关闭了父进程的文件描述符(即引用计数-1),也就是父进程没有在写数据了,但是子进程还在读取数据读进程就会阻塞

通过上面我们知道,从第一个到最后一个,管道的引用计数是越来越少的,最后一个管道的引用计数为1,当我们逆向关闭管道的时候,也能够正确回收子进程

void CleanUpChannel(std::vector<Channel> &channels)
{// 逆向关闭管道int num = channels.size() - 1;while(num >=0){channels[num].CloseChannel();channels[num--].Wait();}
}

4.5、修复bug

 从上面的代码测试可以知道,如果我们关闭写端文件描述符之后再回收子进程,会出现阻塞情况,那么如何解决这个bug呢?

其实很简单,当我们创建第二个子进程的时候,我们就已经保存了前面所有打开的写端描述符,因此我们可以再第二次创建子进程之后,每次都先关闭写端文件描述符。

创建信道和子进程

void CreateChannelAndSub(int num, std::vector<Channel> *channels)
{// Bugfor (int i = 0; i < num; i++){// 1.创建管道int pipefd[2] = {0};int n = pipe(pipefd);// n < 0创建失败if (n < 0)exit(0);// 2.创建子进程pid_t id = fork();// child -- readif (id == 0){// fix bugif(!channels->empty()){// 第二次之后,关闭子进程开始创建的写端管道for(auto &channel : *channels) channel.CloseChannel();}// 关闭写端close(pipefd[1]);//work(pipefd[0]);close(pipefd[0]);exit(0);}// 3.构建一个channel名称std::string channel_name = "channel-" + std::to_string(i);// father -- writeclose(pipefd[0]);channels->push_back(Channel(pipefd[1], id, channel_name));}
}

5、进程池完整代码

5.1、makefile

makefile

processpool:ProcessPool.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -rf processpool

5.2、Task.hpp

.hpp可以将函数声明定义放在一个文件。

Task.hpp

#pragma once#include <iostream>
#include <ctime>
#include <cstdlib>
#include <sys/types.h>
#include <unistd.h>// 任务个数
#define TaskNum 3typedef void (*task_t)();//函数指针重命名 void Print()
{std::cout << "I am a Print task" << std::endl;
}
void DownLoad()
{std::cout << "I am a Download task" << std::endl;
}
void Flush()
{std::cout << "I am a Flush task" << std::endl;
}// 创建任务数组
task_t tasks[TaskNum]; // 加载任务
void LoadTask()
{srand(time(nullptr) ^ getpid() ^1777);tasks[0] = Print;tasks[1] = DownLoad;tasks[2] = Flush;
}// 通过数组下标执行某一条任务
void ExecuteTask(int number)
{if(number < 0 || number > 2) return;tasks[number]();
}// 通过随机数选择任务编号
int SelectTask()
{return rand() % TaskNum;
}void work()
{while (true){int command = 0;// 从标准输入读数据int n = read(0, &command, sizeof(command));if (n == sizeof(int)){std::cout << "pid is : " << getpid() << " handler task" << std::endl;ExecuteTask(command);}else if (n == 0){std::cout << "sub process : " << getpid() << " quit" << std::endl;break;}}
}

5.3、ProcessPool.cc

ProcessPool.cc

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp"// void work(int fd)
// {
//     while(true)
//     {
//         sleep(1);
//     }
// }// void work(int rfd)
// {
//     while (true)
//     {
//         int command = 0;
//         int n = read(rfd, &command, sizeof(command));
//         if (n == sizeof(int))
//         {
//             std::cout << "pid is : " << getpid() << " handler task" << std::endl;
//             ExecuteTask(command);
//         }
//         else if (n == 0)
//         {
//             std::cout << "sub process : " << getpid() << " quit" << std::endl;
//             break;
//         }
//     }
// }
// master信道
class Channel
{
public:Channel(int wfd, pid_t id, const std::string &name): _wfd(wfd), _subprocessid(id), _name(name){}int GetWfd() const { return _wfd; }pid_t GetProcessId() const { return _subprocessid; }std::string GetName() const { return _name; }void CloseChannel(){close(_wfd);}void Wait(){pid_t rid = waitpid(_subprocessid, nullptr, 0);if (rid > 0){std::cout << "wait " << rid << " success" << std::endl;}}~Channel(){}private:int _wfd;            // 写端fdpid_t _subprocessid; // 子进程pidstd::string _name;   // 信道名字
};// 形参类型和命名规范
// const &: 输出
// & : 输入输出型参数
// * : 输出型参数
// // 创建信道和子进程
// void CreateChannelAndSub(int num, std::vector<Channel> *channels)
// {
//     // Bug
//     for (int i = 0; i < num; i++)
//     {
//         // 1.创建管道
//         int pipefd[2] = {0};
//         int n = pipe(pipefd);
//         // n < 0创建失败
//         if (n < 0)
//             exit(0);//         // 2.创建子进程
//         pid_t id = fork();//         // child -- read
//         if (id == 0)
//         {
//             // fix bug
//             if(!channels->empty())
//             {
//                 // 第二次之后,关闭子进程开始创建的写端管道
//                 for(auto &channel : *channels) 
//                     channel.CloseChannel();
//             }
//             // 关闭写端
//             close(pipefd[1]);
//             //
//             work(pipefd[0]);
//             close(pipefd[0]);
//             exit(0);
//         }
//         // 3.构建一个channel名称
//         std::string channel_name = "channel-" + std::to_string(i);
//         // father -- write
//         close(pipefd[0]);
//         channels->push_back(Channel(pipefd[1], id, channel_name));
//     }
// }// 创建信道和子进程(回调方式)
void CreateChannelAndSub(int num, std::vector<Channel> *channels,task_t task)
{// Bugfor (int i = 0; i < num; i++){// 1.创建管道int pipefd[2] = {0};int n = pipe(pipefd);// n < 0创建失败if (n < 0)exit(0);// 2.创建子进程pid_t id = fork();// child -- readif (id == 0){// fix bugif(!channels->empty()){// 第二次之后,关闭子进程开始创建的写端管道for(auto &channel : *channels) channel.CloseChannel();}// 关闭写端close(pipefd[1]);dup2(pipefd[0], 0); // 将管道的读端,重定向到标准输入task();close(pipefd[0]);exit(0);}// 3.构建一个channel名称std::string channel_name = "channel-" + std::to_string(i);// father -- writeclose(pipefd[0]);channels->push_back(Channel(pipefd[1], id, channel_name));}
}// 0 1 2 channelnum
int NextChannel(int channelnum)
{static int next = 0;int channel = next;next++;next %= channelnum;return channel;
}void SendTaskCommand(const Channel &channel, int taskcommand)
{write(channel.GetWfd(), &taskcommand, sizeof(taskcommand));
}// 控制一次子进程
void ctrlProcessOnce(std::vector<Channel> &channels)
{sleep(1);// a.选择一个任务int taskcommand = SelectTask();// b.选择一个信道和进程int channel_index = NextChannel(channels.size());// c.发送任务SendTaskCommand(channels[channel_index], taskcommand);std::cout << std::endl;std::cout << "taskcommand: " << taskcommand << " channel: "<< channels[channel_index].GetName() << " sub process: " << channels[channel_index].GetProcessId() << std::endl;
}// void ctrlProcess(std::vector<Channel> &channels)
// {
//     while (true)
//     {
//         sleep(1);
//         // a.选择一个任务
//         int taskcommand = SelectTask();
//         // b.选择一个信道和进程
//         int channel_index = NextChannel(channels.size());
//         // c.发送任务
//         SendTaskCommand(channels[channel_index], taskcommand);
//         std::cout << std::endl;
//         std::cout << "taskcommand: " << taskcommand << " channel: "
//                   << channels[channel_index].GetName() << " sub process: " << channels[channel_index].GetProcessId() << std::endl;
//     }
// }void ctrlProcess(std::vector<Channel> &channels, int times = -1)
{if (times > 0){while (times--){ctrlProcessOnce(channels);}}else{while (true){ctrlProcessOnce(channels);}}
}void CleanUpChannel(std::vector<Channel> &channels)
{// 逆向关闭管道int num = channels.size() - 1;while(num >=0){channels[num].CloseChannel();channels[num--].Wait();}// for (auto &channel : channels)// {//     channel.CloseChannel();//     channel.Wait();// }// for (auto &channel : channels)// {//     channel.Wait();// }
}int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << "processnum" << std::endl;return 1;}int num = std::stoi(argv[1]);// 加载任务LoadTask();std::vector<Channel> channels;// 1.创建信道和子进程,普通版本//CreateChannelAndSub(num, &channels);// 1.创建信道和子进程,回调版本,调用什么函数,传什么函数名CreateChannelAndSub(num, &channels,work);// 2.通过channle控制子进程,执行5次ctrlProcess(channels,5);// 3. 回收管道和子进程. a. 关闭所有的写端 b. 回收子进程CleanUpChannel(channels);return 0;
}// ./processpool 5 测试代码一
// int main(int argc,char* argv[])
// {
//     // 通过命令行传参创建几个子进程,不传个数直接报错
//     if(argc != 2)
//     {
//         std::cerr << "Usage: " << argv[0] << "processnum" << std::endl;
//         return 1;
//     }
//     int num = std::stoi(argv[1]);//     std::vector<Channel> channels;
//     // 1.创建信道和子进程
//     for(int i = 0;i < num;i++)
//     {
//         // 1.创建管道
//         int pipefd[2] = {0};
//         int n = pipe(pipefd);
//         // n < 0创建失败
//         if(n < 0) exit(0);//         // 2.创建子进程
//         pid_t id = fork();//         // child -- read
//         if(id == 0)
//         {
//             // 关闭写端
//             close(pipefd[1]);
//             //
//             work(pipefd[0]);
//             close(pipefd[0]);
//             exit(0);
//         }
//         // 3.构建一个channel名称
//         std::string channel_name = "channel-" + std::to_string(i);
//         // father -- write
//         close(pipefd[0]);
//         channels.push_back(Channel(pipefd[1],id,channel_name));
//     }
//     // for test
//     for(auto& channel : channels)
//     {
//         std::cout << "====================================" << std::endl;
//         std::cout << "channel_name->" << channel.GetName() << std::endl;
//         std::cout << "channel_wfd->" << channel.GetWfd() << std::endl;
//         std::cout << "channel_id->" << channel.GetProcessId() << std::endl;
//     }
//     sleep(100);
//     return 0;
// }

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

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

相关文章

一文读懂JPA及Mybatis的原理和机制(面试经)

导览 前言Q&#xff1a;什么是JPA1. 简介2. 原理 Q&#xff1a;JPA持久化框架—Mybatis1. 内部组成与关系2. 各组件作用域和生命周期3. 动态SQL3.1 if语句3.2 choose-when-otherwise 4. mapper映射XML4.1 select4.2 insert 5. $与#的区别5.1 #{...}5.2 ${...} 结语精彩回顾 前言…

明日周刊-第23期

十月已过半&#xff0c;气温也转凉了&#xff0c;大家注意保温哦。冬吃萝卜&#xff0c;夏吃姜&#xff0c;在快要到来的冬季大家可以选择多吃点萝卜。 配图是本周末去商场抓娃娃的时候拍的照片&#xff0c;现在抓娃娃单次普遍都控制在1块钱以下了&#xff0c;还记得多年前的抓…

qt继承结构

一、 继承结构 所有的窗口类均继承自QWidget类&#xff0c;因此QWidget类本身包含窗口的特性。QWidget对象本身既可以作为独立窗口&#xff0c;又可以作为组件&#xff08;子窗口&#xff09;。 通过构造函数可以创建以上两种形态的QWidget&#xff1a; // 参数1&#xff1a;使…

[C#][winform]基于yolov8的道路交通事故检测系统C#源码+onnx模型+评估指标曲线+精美GUI界面

【重要说明】 该系统以opencvsharp作图像处理,onnxruntime做推理引擎&#xff0c;使用CPU进行推理&#xff0c;适合有显卡或者没有显卡windows x64系统均可&#xff0c;不支持macOS和Linux系统&#xff0c;不支持x86的windows操作系统。由于采用CPU推理&#xff0c;要比GPU慢。…

【重学 MySQL】七十一、揭秘数据库魔法——深入探索并引入视图

【重学 MySQL】七十一、揭秘数据库魔法——深入探索并引入视图 视图的定义视图的作用视图的注意事项 在MySQL数据库中&#xff0c;视图&#xff08;View&#xff09;是一种非常强大且灵活的工具&#xff0c;它为用户提供了以更安全、更清晰的方式查看和管理数据的途径。 视图的…

《计算机视觉》—— 换脸

效果如下&#xff1a; 完整代码&#xff1a; import cv2 import dlib import numpy as npJAW_POINTS list(range(0, 17)) RIGHT_BROW_POINTS list(range(17, 22)) LEFT_BROW_POINTS list(range(22, 27)) NOSE_POINTS list(range(27, 35)) RIGHT_EYE_POINTS list(range(36…

【深入解析】ChatGPT各版本在论文写作中的五大表现差异

学境思源&#xff0c;一键生成论文初稿&#xff1a; AcademicIdeas - 学境思源AI论文写作 人工智能在自然语言处理领域已取得了显著进展&#xff0c;尤其是OpenAI的ChatGPT系列在文本生成和理解方面表现突出。然而&#xff0c;不同版本的ChatGPT在论文写作中的表现存在明显差异…

Python 从入门到实战37(进程间通信)

我们的目标是&#xff1a;通过这一套资料学习下来&#xff0c;可以熟练掌握python基础&#xff0c;然后结合经典实例、实践相结合&#xff0c;使我们完全掌握python&#xff0c;并做到独立完成项目开发的能力。 上篇文章我们讨论了通过multiprocessing模块创建进程操作的相关知…

力扣 142.环形链表Ⅱ【详细解释】

一、题目 二、思路 三、代码 /*** Definition for singly-linked list.* class ListNode {* int val;* ListNode next;* ListNode(int x) {* val x;* next null;* }* }*/ public class Solution {public ListNode detectCycle(ListNode hea…

javaWeb项目-ssm+jsp房屋出租管理系统功能介绍

本项目源码&#xff08;点击下方链接下载&#xff09;&#xff1a;java-ssmjsp房屋出租管理系统实现源码(项目源码-说明文档)资源-CSDN文库 项目关键技术 开发工具&#xff1a;IDEA 、Eclipse 编程语言: Java 数据库: MySQL5.7 框架&#xff1a;ssm、Springboot 前端&#xff…

[含文档+PPT+源码等]精品基于springboot实现的原生Andriod大学校园食堂外卖系统App

基于Spring Boot实现的原生Android大学校园食堂外卖系统App的背景可以从以下几个方面进行阐述&#xff1a; 一、项目背景与需求 随着移动互联网技术的快速发展和智能手机的普及&#xff0c;大学生对于便捷、高效的校园生活服务需求日益增长。大学校园食堂作为学生们日常用餐的…

【电商项目】1分布式基础篇

1 项目简介 1.2 项目架构图 1.2.1 项目微服务架构图 1.2.2 微服务划分图 2 分布式基础概念 3 Linux系统环境搭建 查看网络IP和网关 linux网络环境配置 补充P123&#xff08;修改linux网络设置&开启root密码访问&#xff09; 设置主机名和hosts映射 主机名解析过程分析&…

【问题解决】——当出现0xc000007b和缺少mfc140.dll时,该怎么做才能让软件可以打开

目录 事情起因 问题处理 明确定义 填坑之路 最后我是怎么解决的&#xff08;不想看故事直接到这里&#xff09; 事情起因 最近想要重新安装西门子博途来做西门子的一些算法的时候&#xff0c;发现自己软件装的是V15.1的版本&#xff0c;而买的plc1200固件版本要求至少16以…

性能评测第一,阿里开源可商用AI模型Ovis 1.6使用指南,AI多模态大模型首选

什么是 Ovis 1.6 Gemma 2 9B&#xff1f; Ovis 1.6 Gemma 2 9B 是阿里国际AI团队推出的最新多模态大模型&#xff08;Multimodal Large Language Model&#xff0c;MLLM&#xff09;。该模型旨在结构化地对齐视觉和文本嵌入&#xff0c;能够处理和理解多种不同类型的数据输入&…

抑郁症自测量表 API 接口,洞察情绪状态

抑郁症是一种常见的心理疾病&#xff0c;会给患者的生活和工作带来很大的困扰。为了帮助人们更好地了解自己的情绪状态&#xff0c;有一种抑郁症自测量表&#xff08;简称SDS&#xff09;&#xff0c;它是一种能够反映病人主观抑郁症状的自评量表。下面我们将通过调用抑郁症自测…

基于FreeRTOS的LWIP移植

目录 前言一、移植准备工作二、以太网固件库与驱动2.1 固件库文件添加2.2 库文件修改2.3 添加网卡驱动 三、LWIP 数据包和网络接口管理3.1 添加LWIP源文件3.2 Lwip文件修改3.2.1 修改cc.h3.2.2 修改lwipopts.h3.2.3 修改icmp.c3.2.4 修改sys_arch.h和sys_arch.c3.2.5 修改ether…

Linux·文件与IO

1. 回忆文件操作相关知识 我们首先回忆一下关于文件的一些知识。 如果一个文件没有内容&#xff0c;那它到底有没有再磁盘中存在&#xff1f;答案是存在&#xff0c;因为 文件 内容 属性&#xff0c;即使文件内容为空&#xff0c;但属性信息也是要记录的。就像进程的…

硬件产品经理的开店冒险之旅(下篇)

缘起&#xff1a;自己为何想要去寻找职业第二曲线 承接上篇的内容&#xff0c;一名工作13年的普通硬件产品经理将尝试探索第二职业曲线。根本原因不是出于什么高大上的人生追求或者什么职业理想主义&#xff0c;就是限于目前的整体就业形式到了40岁的IT从业人员基本不可能在岗…

【Python】selenium遇到“InvalidArgumentException”的解决方法

在使用try……except 的时候捕获到这个错误&#xff1a; InvalidArgumentException: invalid argument (Session info: chrome112.0.5614.0) 这个错误代表的是&#xff0c;当传入的参数不符合期望时&#xff0c;就会抛出这个异常&#xff1a; InvalidArgumentException: invali…

day-69 使二进制数组全部等于 1 的最少操作次数 II

思路 与3191. 使二进制数组全部等于 1 的最少操作次数 I思路类似&#xff0c;区别在于该题每次将下标i开始一直到数组末尾所有元素反转&#xff0c;所以我们用一个变量可以统计翻转次数 解题过程 从左向右遍历数组的过程中&#xff0c;有两种情况需要进行翻转&#xff1a;1.当…