网站显示时间代码,小程序开发需求方案,石家庄市建设厅官网,小说网站防盗做的好处目录
前言 1. 进程池 1.1 基本结构#xff1a;
1.2. 池化技术 1.3. 思路分析
1.4. 代码实现
总结 前言 上篇文章介绍了管道及其使用#xff0c;本文在管道的基础上#xff0c;通过匿名管道来实现一个进程池#xff1b; 1. 进程池 父进程创建一组子进程#xff0c;子进…目录
前言 1. 进程池 1.1 基本结构
1.2. 池化技术 1.3. 思路分析
1.4. 代码实现
总结 前言 上篇文章介绍了管道及其使用本文在管道的基础上通过匿名管道来实现一个进程池 1. 进程池 父进程创建一组子进程子进程根据父进程的发送的信号来做出相应的操作 1.1 基本结构 master为父进程父进程通过管道向子进程发送对应的信号让子进程执行相关的操作
1.2. 池化技术
为什么要有进程池? 要解答这个问题需要先了解池化技术池化技术是一种常见的优化方法可用于提高计算和存储资源的利用率从而提高系统性能。通过分类和管理资源或任务的池可以实现资源的高效共享和复用 举个最简单的例子:我们在写vector时它有一个扩容操作我们在实现时一般是2倍扩容为什么要多扩容?——为了防止频繁的申请空间池化技术也是类似的优化方法通过一次性申请一定数量的资源然后自己管理这些资源的分配和回收从而减少频繁向操作系统申请资源的次数在操作系统中申请空间、创建进程等操作都需要一定的时间开销。因此频繁地进行这些操作会降低系统的效率 进程池会提前创建一定数量的进程并保存在进程池中当需要使用新的进程时可以直接从进程池中获取已经存在的空闲进程来执行任务而不需要每次都创建新的进程从而减少了创建和销毁进程的开销
注意 在实现上把任务分配给不同的信道一定要平均不能是有的信道很忙有的信道很闲这样也无法提高效率 在此之前为了便于理解这里再次回顾一下管道的特点及几种不同的情况
a. 管道的4种情况
正常情况如果管道没有数据了读端必须等待直到有数据为止(写端写入数据了)正常情况如果管道被写满了写端必须等待直到有空间为止(读端读走数据)写端关闭读端一直读取, 读端会读到read返回值为0 表示读到文件结尾读端关闭写端一直写入OS会直接杀掉写端进程通过想目标进程发送SIGPIPE(13)信号终止目标进程
b. 匿名管道的5种特性
匿名管道,可以允许具有血缘关系的进程之间进行进程间通信常用与父子,仅限于此匿名管道默认给读写端要提供同步机制 --- 了解现象就行面向字节流的 --- 了解现象就行管道的生命周期是随进程的管道是单向通信的半双工通信的一种特殊情况 1.3. 思路分析 要创建一个管道用于父进程于子进程的通信简单 问题在于后续管道的创建
结构图如下 在创建第二个管道时父进程新建子进程子进程继承父进程的属性然后关闭父进程的读端子进程的写端构建单向信道 问题就出在这里看上图结构子进程会继承父进程属性所以第二个子进程的3号文件描述符也会指向的1号管道的写端正常情况下是不能指定的 以此类推第三个进程也会指向1号管道和2号管道只有最后一个管道是只有一个写端指向其余的管道都有多个写端
在实际上不会出现子进程向管道写入的情况但是在关闭管道的时候容易出问题不注意就会导致程序阻塞
正常的关闭信道这样写
for (const auto e : c)
{close(e.ctrlfd);waitpid(e.workerid, nullptr, 0);
} 遍历这个数组关闭父进程对每个管道的写端看似很完美而实际情况是 除最后一个管道其余管道依然会有写端指向而只有当所有写端都关闭后调用read函数时才会返回0表示已经读取到文件末尾;此时表示执行完毕会进入下一步的回收 如果存在写端并且写端一直不写入数据在这种情况下read函数不会返回而是一直等待数据到来。读端(子进程)的read函数会一直阻塞直到有数据写入为止这也就会导致程序阻塞住
如何解决
方法一 倒着遍历去关闭管道 倒着去关闭最后一个管道没有写端管道正常关闭回收子进程子进程被回收文件描述符也会被释放其余管道的写端就会减少依次关闭就可以保证所有管道正常关闭 方法二 借助数据结构去解决记录父进程中的写端fd在子进程中依次关闭这样创建出来的信道之间连接关系是理想的关系不会那么乱 父进程中创建一个临时vector父进程记录每个管道写端fd把数据存储道临时vector中一份;这样每个子进程拿到的就是当前父进程所有写端的fd子进程全部关掉即可;
1.4. 代码实现
模拟执行任务编写一个任务类
// Task.hpp
#pragma once#include iostream
#include functional
#include vector
#include ctimetypedef 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 Tasklist
{public:Tasklist(){tasks.push_back(Download);tasks.push_back(PrintLog);tasks.push_back(PushVideoStream);// 模拟随机任务srand(time(nullptr));}// 检查信号是否合法bool CheckSafe(int code){if (code 0 code tasks.size())return true;elsereturn false;}// 执行任务void RunTask(int code){return tasks[code]();}// 随机选择任务int SelectTask(){return rand() % tasks.size();}// 信号转换std::string ToDesc(int code){switch (code){case g_download_code:return Download;case g_printlog_code:return PrintLog;case g_push_videostream_code:return PushVideoStream;default:return Unknow;}}
public:const static int g_download_code 0;const static int g_printlog_code 1;const static int g_push_videostream_code 2;std::vectortask_t tasks;
};Tasklist init;//定义对象 进程池
#include iostream
#include vector
#include string
#include unistd.h
#include assert.h
#include sys/wait.h
#include sys/types.h
#include Task.hppconst int nums 5;
static int number 1;// 将管道描述成一个对象
class channel
{
public:channel(int fd, pid_t id):ctrlfd(fd), workerid(id){name channel std::to_string(number);}
public:int ctrlfd;pid_t workerid;//进程idstd::string name;};
// 工作接口
void work()
{while (true){int code 0;// 该接口由子进程执行// 从标准输入去读其实就是从管道去读// 为了方便读数据所以将对应管道读端重定向到标准输入否则还需记录子进程对应管道的读端ssize_t n read(0, code, sizeof(code));//assert(n sizeof(code)); //父进程一旦退出子进程也会退出没有数据写入读到的字节是0assert强制停止// 判断读到的数据是否正常if (n sizeof(code)){if (!init.CheckSafe(code)) continue;init.RunTask(code);}else if (n 0){break;}else{}}std::cout child quit std::endl;
}void PrintFd(const std::vectorint fds)
{std::cout getpid() close fds: ;for (auto fd : fds){std::cout fd ;}std::cout std::endl;
}// 创建管道
void CreateChannel(std::vectorchannel* c)
{std::vectorint tmp;for (int i 0; i nums; i){// 创建信道int pipefd[2];int n pipe(pipefd);//管道建立成功返回0失败返回错误码assert(n 0);(void)n;// 创建进程pid_t id fork();assert(id 0); // 条件为假返回报错信息// 构建单向信道if (id 0) //child{if (!tmp.empty()){// 方法二// 关闭其余写端的fdfor (auto e : tmp){close(e);}PrintFd(tmp);}// 这里并没有重复关闭先创建的管道再创建子进程然后再关闭将写端fd加入到tmp// 这里是关闭子进程对当前管道的写端close(pipefd[1]);// 将标准输入重定向到管道读端管道从标准输入中读数据dup2(pipefd[0], 0);// TODOwork();exit(0);}// fatherclose(pipefd[0]); //关闭父进程读// 将管道存储起来方便后续管理c-push_back(channel(pipefd[1], id));tmp.push_back(pipefd[1]);}
}void SendCommand(const std::vectorchannel c, bool flag, int nums -1)
{int pos 0;while (true){//1.选择任务int command init.SelectTask();//2.选择信道const auto channel c[pos];pos % c.size();//debugstd::cout send command init.ToDesc(command) [ command ] in channel.name worker is : channel.workerid std::endl;//3.发送任务write(channel.ctrlfd, command, sizeof(command));// 判断任务执行完是否退出if (!flag){nums--;if (nums 0)break;}sleep(1);}std::cout SendCommand done... std::endl;
}
void ReleaseChannel(std::vectorchannel c)
{//倒着回收// int n c.size() - 1;// for(; n 0; n--)// {// close(c[n].ctrlfd);// waitpid(c[n].workerid, nullptr, 0);// }for (const auto e : c){close(e.ctrlfd);waitpid(e.workerid, nullptr, 0);}
}const bool g_always_loop true;
int main()
{std::vectorchannel channels;//创建进程创建信道CreateChannel(channels);//开始执行任务SendCommand(channels, !g_always_loop, 10);//回收资源等待子进程退出ReleaseChannel(channels);return 0;
} 结构并不复杂这里只是一个简单的示例这个示例比较考验对多进程编程
1.5. 思考
在进程池体系中如果一个子进程退出了一个管道的读端关闭会怎样 如果父进程知道子进程已经退出即通过监控子进程状态并处理子进程退出的情况下父进程在得知子进程退出后就不会再继续向已经退出的子进程的管道中写入数据也不会因为收到信号而终止 如果父进程没有正确地监控子进程的状态不知道子进程已经退出那么当父进程尝试向已经退出的子进程的管道中写入数据时会收到SIGPIPE信号而终止。在这种情况下剩余的子进程会成为孤儿进程由操作系统接管
因此在设计时也可以进行特殊处理处理子进程退出避免父进程被OS杀死的情况如何去处理 可以在选择信道那里多一步判断判断信道对应的子进程是否已经退出通过信道描述类找到对应的进程id通过waitpid的非阻塞等待判断是否退出 总结 以上便是本文的全部内容希望对你有所帮助或启发感谢阅读