尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Linux:进程池的简单实现

Linux:进程池的简单实现 目录1. 主函数2. 要执行的任务3. 创建信道4. 发送消息5. 回收管道创建和销毁进程需要使用系统调用操作系统分配资源内存、文件描述符等开销较大。我们可以提前创建好一些进程当操作系统需要完成一些任务时可以直接分配不用再单独一个个创建减小一部分开销。内存池申请的空间往往是大于你要求的可能你暂时用不到想用的时候就可以直接用不用再申请空间。但是这些空间是需要交给用户管理的我们使用 channel 描述管理进行管道class channel//先描述再组织 { public: channel(int fd,pid_t id):ctrlfd(fd),workerid(id) { name channel-std::to_string(number); } int ctrlfd;//父进程中对应的文件描述符 pid_t workerid;//子进程id std::string name;//管道名 };1. 主函数我们创建一个信道集合 channels 方便统一管理。然后创建信道父进程发送任务子进程执行之后父进程关闭写端回收子进程回收资源。int main() { std::vectorchannel channels; //1.创建信道创建进程 CreateChannels(channels); //2.父进程发送任务 const bool g_always_loop true; //SendCommand(channels,g_always_loop);//一直运行 SendCommand(channels,!g_always_loop,10);//发送10次任务 //3.回收资源 ReleaseChannels(channels); return 0; }2. 要执行的任务这里模拟定义了4种任务我们调用采用随机执行的方法。#pragma once #include iostream #include functional #include vector #include unistd.h //using task_t std::functionvoid; typedef std::functionvoid() task_t; void Download() { std::cout我是一个下载任务,处理者 getpid() std::endl; } void PrintLog() { std::cout我是一个打印日志任务,处理者 getpid() std::endl; } void PushVideoStream() { std::cout我是一个推送视频任务,处理者 getpid() std::endl; } class Init { public: // 任务集合 std::vectortask_t tasks; //成员函数可以访问静态成员静态不能访问非静态 // 任务集合 -- 没用到 const static int g_download_code 0; const static int g_printLog_code 1; const static int g_push_video_stream_code 2; public: Init() { tasks.push_back(Download); tasks.push_back(PrintLog); tasks.push_back(PushVideoStream); srand(time(nullptr)^getpid());//更随机 } bool CheckSafe(int code) { if(code 0 code 3) return true; else return false; } void RunTask(int code) { tasks[code](); } int SelectTask()//随机选择任务 { return rand()%tasks.size(); } std::string ToDes(int code) { switch (code) { case g_download_code: return download; break; case g_printLog_code: return PrintLog; break; case g_push_video_stream_code: return push_video_stream; break; default: return ; break; } } }; //静态成员类外定义或者静态函数中push_back //std::vectortask_t Init::tasks {Download, PrintLog, PushVideoStream}; Init init;//定义对象3. 创建信道我们这里创建有 5 个子进程的进程池。temp 用来存储父进程打开的所有子进程写端它的作用可以让每个子进程关闭父进程之前创建的其他子进程读端 fd即与自己无关的文件描述符为什么我们在后面回收管道中讲。然后对于每个子进程我们将标准输入重定向到读端文件以后子进程读端读数据只需要从fd 0中 read 读去就行。最后对每个管道都添加到 channels 进程池中方便管理。const int num 5; static int number 1; //创建信道创建进程 void CreateChannels(std::vectorchannel* c) { //bug std::vectorint temp; for(int i 0;inum;i) { //1.先创建管道 int pipefd[2]; int n pipe(pipefd);//不能传入pipefd[2],pipefd[2]是一个pipefd数组的第3个值已经越界. assert(n 0); //2.创建进程 pid_t id fork(); assert(id ! -1); //3.构建单向通信的信道 //3.1 子进程 子读父写 if(id 0) { if(!temp.empty())//每个子进程关闭 父进程之前创建的其他子进程读端fd 即与自己无关的文件描述符 { for(auto fd : temp) { close(fd); } PrintFd(temp); } close(pipefd[1]); dup2(pipefd[0],0);//将管道文件 重定向 到 标准输入中, 管道文件就是输入 Work(); exit(0); } //3.2 父进程 关闭读 close(pipefd[0]); //这样并不知道 具体管道 对应 哪个子进程 temp.push_back(pipefd[1]);// //对每个管道命名管理 c-push_back(channel(pipefd[1],id));//push_back只接受一个参数只有单参数自定义类型才会隐式类型转换 } }对于 Work() 函数即子进程执行任务的函数我们父进程发送一个4字节的整数判断执行不同的任务。void Work() { while(true) { int code 0; //可以直接发送整形即二进制也可以显示使用“”发送字符串再stoi int n read(0,code,sizeof(code));//从fd0中读每次期望读4个字节, 判断执行什么任务 if(n sizeof(code))//要读到4个字节 { if (!init.CheckSafe(code)) // code时读到的值n是读到的个数 continue; init.RunTask(code); // 执行对应任务 } else if(n 0) { break; } } std::cout child quitstd::endl; }关于read默认是阻塞模式当写端关闭才会返回0这里如果父进程退出曾经为每一个子进程创建的写端描述符就关闭了read就会返回0表示读到0字节数据读到文件结尾然后自己进程判断后也会关闭所以我们可以通过主动关闭写端可以控制子进程退出。场景阻塞模式默认非阻塞模式O_NONBLOCK管道/套接字缓冲区为空但对端写端仍打开read阻塞不返回直到有数据立即返回-1errno设为EAGAIN或EWOULDBLOCK对端写端已关闭EOF返回0表示没有更多数据返回04. 发送消息我们这里选择轮询管道随机任务。flag true 用来表示一直发送信息给子进程。void SendCommand(const std::vectorchannel c, bool flag, int num -1)//随机任务轮询管道使用 { int pos 0; while(true) { // 1.选择任务 int command init.SelectTask(); // 2.选择信道 const channel p c[pos];//c是指针时不是引用所以c[pos]等价于*(cpos) 还是vectorchannel类型 pos % c.size(); //debug std::cout send command init.ToDes(command) [ command ] in p.name worker is: p.workerid std::endl; // 3.发送任务 write(p.ctrlfd, command, sizeof(command)); //4.判断是否退出 if(flag false) { num--; if(num 0) break; } sleep(1); } std::cout send done std::endl; }5. 回收管道这里讲一下为什么需要使用 std::vectorint temp 记录一下父进程创建曾经创建的子进程。父进程在创建子进程时子进程会复制父进程的文件描述符表所以如果不关闭父进程之前创建的管道文件对应的文件描述符之后创建的子进程也可以看到之前父进程创建的管道效果如下图所示channel1 有 3 个子进程可以写channel2有两个子进程可以写。当以这种方式创建多个子进程时默认情况下父进程按顺序从上到下关闭写端时read是不返回0的所以需要先把父进程的管道全部关闭然后子进程自底向上退出。void ReleaseChannels(const std::vectorchannel channels) { //verson1:回收资源,想让子进程退出并且释放管道只要关闭写端 for(const auto c:channels) { close(c.ctrlfd); } for(const auto c:channels) { //构建channel是保存子进程pid就是方便这里回收 //这里阻塞等待都会被等待到但实际还是子进程先变成僵尸进程 pid_t rid waitpid(c.workerid,nullptr,0); if(rid c.workerid) { std::cout wait child c.workerid std::endl; } } } void ReleaseChannels(const std::vectorchannel channels) { //version2: 倒状回收,先关闭最后一个 int num channels.size()-1; for(;num0;num--) { close(channels[num].ctrlfd); waitpid(channels[num].workerid,nullptr,0); } }我们文章第三部分创建信道时子进程已经关闭多余的文件描述符可以直接 version1 顺序释放。如果不关闭多余文件描述符直接version1 顺序回收会阻塞。注意读写操作都是原子操作不用担心正在写时数据的一半被读取当一端操作时另一端必须等待上一篇文章提到管道大小64kb也不用担心写数据写不完。本篇结束
返回列表