Linux:进程池的简单实现
目录
创建和销毁进程需要使用系统调用,操作系统分配资源(内存、文件描述符等),开销较大。我们可以提前创建好一些进程,当操作系统需要完成一些任务时可以直接分配,不用再单独一个个创建,减小一部分开销。
内存池:申请的空间往往是大于你要求的,可能你暂时用不到,想用的时候就可以直接用,不用再申请空间。但是这些空间是需要交给用户管理的,我们使用 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::vector<channel> 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::function<void>;
typedef std::function<void()> 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::vector<task_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::vector<task_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::vector<channel>* c)
{
//bug
std::vector<int> temp;
for(int i = 0;i<num;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));//从fd=0中读,每次期望读4个字节, 判断执行什么任务
if(n == sizeof(code))//要读到4个字节
{
if (!init.CheckSafe(code)) // code时读到的值,n是读到的个数
continue;
init.RunTask(code); // 执行对应任务
}
else if(n == 0)
{
break;
}
}
std::cout << "child quit"<<std::endl;
}
关于read,默认是阻塞模式,当写端关闭才会返回0,这里如果父进程退出,曾经为每一个子进程创建的写端描述符就关闭了,read就会返回0,表示读到0字节数据,读到文件结尾,然后自己进程判断后也会关闭,所以我们可以通过主动关闭写端可以控制子进程退出。
| 场景 | 阻塞模式(默认) | 非阻塞模式(O_NONBLOCK) |
|---|---|---|
| 管道/套接字缓冲区为空,但对端写端仍打开 | read 阻塞,不返回,直到有数据 | 立即返回 -1,errno 设为 EAGAIN 或 EWOULDBLOCK |
| 对端写端已关闭(EOF) | 返回 0(表示没有更多数据) | 返回 0 |
4. 发送消息
我们这里选择轮询管道,随机任务。flag = true 用来表示一直发送信息给子进程。
void SendCommand(const std::vector<channel> &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]等价于*(c+pos) 还是vector<channel>类型
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::vector<int> temp 记录一下父进程创建曾经创建的子进程。父进程在创建子进程时,子进程会复制父进程的文件描述符表,所以如果不关闭父进程之前创建的管道文件对应的文件描述符,之后创建的子进程也可以看到之前父进程创建的管道,效果如下图所示:channel1 有 3 个子进程可以写,channel2有两个子进程可以写。
当以这种方式创建多个子进程时,默认情况下父进程按顺序从上到下关闭写端时,read是不返回0的,所以需要先把父进程的管道全部关闭,然后子进程自底向上退出。

void ReleaseChannels(const std::vector<channel> 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::vector<channel> channels)
{
//version2: 倒状回收,先关闭最后一个
int num = channels.size()-1;
for(;num>=0;num--)
{
close(channels[num].ctrlfd);
waitpid(channels[num].workerid,nullptr,0);
}
}
我们文章第三部分创建信道时,子进程已经关闭多余的文件描述符,可以直接 version1 顺序释放。如果不关闭多余文件描述符,直接version1 顺序回收,会阻塞。
注意:读写操作都是原子操作,不用担心正在写时数据的一半被读取,当一端操作时另一端必须等待,上一篇文章提到管道大小64kb,也不用担心写数据写不完。
本篇结束!
更多推荐



所有评论(0)