目录
- 一、进程池的逻辑框架
-
- [1.1 管理管道和子进程](#1.1 管理管道和子进程)
- [1.2 创建多个管道和子进程](#1.2 创建多个管道和子进程)
- [1.3 改造代码 并 新建进程池类](#1.3 改造代码 并 新建进程池类)
- 二、父进程控制子进程
-
- [2.1 选择一个子进程](#2.1 选择一个子进程)
- [2.2 选择一个任务](#2.2 选择一个任务)
- [2.3 父进程发送任务给子进程](#2.3 父进程发送任务给子进程)
- 三、释放和回收资源
- 四、源代码

个人主页:矢望
个人专栏:C++、Linux、C语言、数据结构、Coze-AI、MySQL
一、进程池的逻辑框架
1.1 管理管道和子进程
上期博客我们提到了主从模式,也就是主从模式进程池,今天我们来编写一下代码。
首先要让一个进程对多个子进程做管理,就需要让父进程创建多个管道,并且创建出多个子进程,那么为了分清楚那个子进程对应着哪个管道,所以我们需要一个类对它们进行管理。我们把这个类叫做通道类Channel,用它来管理匿名管道的写端文件描述符和对应管道另一端的子进程。
cpp
// 通道类,管理匿名管道和读端的子进程
class Channel
{
public:
Channel()
{}
~Channel()
{}
private:
int _wfd; // 写端的文件描述符
pid_t _sub_pid; // 管道读端子进程的 pid
std::string _sub_name; // 关于子进程的描述
};
1.2 创建多个管道和子进程
要进行进程池,首先就需要有多个管道和进程在此待命,所以我们需要创建若干个管道和子进程。
我们可以将要创建的数量定义成全局变量,然后使用循环批量创建。还可以使用自定义退出码的方式自己设定返回值的含义。
cpp
// 自定义退出码含义
enum
{
OK,
PIPE_ERROR,
FORK_ERROR
};
const int process_num = 5;
void Do_Task(int fd) // 子进程的入口函数
{}
// ...
int main()
{
// 1. 创建多个管道和多个子进程
for(int i = 1; i <= process_num; i++)
{
int pipefd[2];
int n = pipe(pipefd); // 创建管道
if(n < 0)
{
std::cerr << "pipe create error!" << std::endl; // 向标准错误写
exit(PIPE_ERROR);
}
pid_t id = fork(); // 创建子进程
if(id < 0)
{
std::cerr << "fork error!" << std::endl; // 向标准错误写
exit(FORK_ERROR);
}
else if(id == 0)
{
// 子进程
close(pipefd[1]); // 子进程关闭管道写端
Do_Task(pipefd[0]); // 子进程通过读端的文件描述符获取任务
exit(OK); // 子进程执行完任务直接退出
}
// 父进程
close(pipefd[0]); // 父进程关闭管道读端
}
return 0;
}
如上,我们完成了创建多个管道和子进程的工作,也完成了子进程要执行任务的雏形Do_Task。还做了一些执行失败时的差错处理。将来子进程就会在if(id == 0)的{}中完成它的工作,如果父进程没有给它派发任务,它就一直等。父进程派发任务,子进程就执行任务,执行完成之后就退出。
当然还有一些没有完善。
仔细体会上面的代码,结合相关进程要执行的工作,我们发现,子进程它关闭了写端,并且拿到了读端,将读端的文件描述符交给了Do_Task函数,将来就可以通过它来获取任务了。
可是父进程它虽然关闭了读端,但是它的写端没有被管理起来,等到下一个循环就会被覆盖丢失。也就是父进程未来不知道通过哪个文件描述符给子进程传递任务,所以我们需要将它们管理起来。
我们前面已经创建出了相关的管理通道类,就是为了这一步。
cpp
// 通道类,管理匿名管道和读端的子进程
class Channel
{
public:
Channel(int wfd, pid_t sub_pid)
:_wfd(wfd)
,_sub_pid(sub_pid)
{
_sub_name = "子进程:" + std::to_string(sub_pid);
}
~Channel()
{}
private:
int _wfd; // 写端的文件描述符
pid_t _sub_pid; // 管道读端子进程的 pid
std::string _sub_name; // 关于子进程的描述
};
int main()
{
// 0. 组织Channel的容器
std::vector<Channel> channels;
// 1. 创建多个管道和多个子进程
for(int i = 1; i <= process_num; i++)
{
// ...
// 父进程
close(pipefd[0]); // 父进程关闭管道读端
Channel ch(pipefd[1], id);
channels.push_back(ch); // 管理每个管道和对应的子进程
}
return 0;
}
这样我们就创建出了多个管道和子进程,未来就可以让父进程通过channels控制子进程了。
代码测试
我们这里通过打印来进行代码测试。
cpp
class Channel
{
public:
Channel(int wfd, pid_t sub_pid)
:_wfd(wfd)
,_sub_pid(sub_pid)
{
_sub_name = "子进程:" + std::to_string(sub_pid);
}
~Channel()
{}
void PrintInfo()
{
printf("父进程管理:wfd -> %d, pid -> %d, 描述 -> %s\n", _wfd, _sub_pid, _sub_name.c_str());
}
private:
int _wfd; // 写端的文件描述符
pid_t _sub_pid; // 管道读端子进程的 pid
std::string _sub_name; // 关于子进程的描述
};
for(int i = 1; i <= process_num; i++)
{
// ...
std::cout << "创建子进程:" << id << " 成功..." << std::endl;
sleep(1); // 间隔 1 秒创建一次
}
for(auto& c : channels)
{
c.PrintInfo();
}
sleep(100); // 让父进程休眠
编译测试:

如上图,成功创建了并且父进程也使用channels将它们管理起来了。
1.3 改造代码 并 新建进程池类
现在代码能够正常运行,这没有问题。但既然我们在写进程池,不如,我们就创建一个进程池类。
由于父进程通过channels实现对子进程的控制,所以我们可以将channels容器变成这个进程池类的私有成员,而我们的第一步创建多个管道和多个子进程过程所执行的工作,可以封装成为进程池类的一个函数,将来方便管理。
这样就初步有了对这个进程池类的设想,将来我不希望将创建函数暴露出去,我可以使用Init()函数完成创建多个管道和多个子进程的工作。而我们上一个阶段的代码测试的部分可以成为进程池类中的一个DeBug函数。我们直接调用即可。
因此,进程池类:
cpp
class ProcessPool
{
public:
ProcessPool()
{}
~ProcessPool()
{}
void Init()
{
CreateProcessChannel();
}
void DeBug()
{
for(auto& c : channels)
{
c.PrintInfo();
}
}
private:
void CreateProcessChannel()
{
// 1. 创建多个管道和多个子进程
for(int i = 1; i <= process_num; i++)
{
int pipefd[2];
int n = pipe(pipefd); // 创建管道
if(n < 0)
{
std::cerr << "pipe create error!" << std::endl; // 向标准错误写
exit(PIPE_ERROR);
}
pid_t id = fork(); // 创建子进程
if(id < 0)
{
std::cerr << "fork error!" << std::endl; // 向标准错误写
exit(FORK_ERROR);
}
else if(id == 0)
{
// 子进程
close(pipefd[1]); // 子进程关闭管道写端
Do_Task(pipefd[0]); // 子进程通过读端的文件描述符获取任务
exit(OK); // 子进程执行完任务直接退出
}
// 父进程
close(pipefd[0]); // 父进程关闭管道读端
channels.emplace_back(pipefd[1], id); // 管理每个管道和对应的子进程
// 上面的写法更优,作用等价于下面的
// Channel ch(pipefd[1], id);
// channels.push_back(ch); // 管理每个管道和对应的子进程
std::cout << "创建子进程:" << id << " 成功..." << std::endl;
sleep(1); // 间隔 1 秒创建一次
}
}
private:
// 0. 组织Channel的容器
std::vector<Channel> channels;
};
测试代码:
cpp
int main()
{
// 0.创建并初始化进程池
ProcessPool pp;
pp.Init();
pp.DeBug();
sleep(100); // 让父进程休眠
return 0;
}
编译运行 :

此外,如果你不想暴露你的通道类Channel,你可以将它变成进程池类的内部类,这里就不再进行。
接下来还有做一层改造,现在我们的子进程执行任务时,直接就死板的调用了Do_Task函数,如果将来想让子进程执行其它函数,还要手动修改相关代码吗?
所以子进程在执行任务的时候可以采用回调的方式,由父进程给子进程安排任务,而子进程不知道自己要执行什么任务。
这里我们采用包装器来实现回调。
cpp
// 定义任务函数类型 - 带 int 参数
using task_t = std::function<void(int)>;
void Do_Task(int fd) // 子进程的入口函数
{}
class ProcessPool
{
public:
// ...
void Init(task_t cb) // 初始化进程池,设置任务处理回调
{
CreateProcessChannel(cb);
}
private:
void CreateProcessChannel(task_t cb)
{
// 1. 创建多个管道和多个子进程
for(int i = 1; i <= process_num; i++)
{
// ...
else if(id == 0)
{
// 子进程
close(pipefd[1]); // 子进程关闭管道写端
// 回调
cb(pipefd[0]); // 子进程通过读端的文件描述符获取任务
exit(OK); // 子进程执行完任务直接退出
}
// ...
}
}
private:
// 0. 组织Channel的容器
std::vector<Channel> channels;
};
cpp
int main()
{
// 0.创建并初始化进程池
ProcessPool pp;
pp.Init(Do_Task);
pp.DeBug();
sleep(100); // 让父进程休眠
return 0;
}
再次编译运行:

二、父进程控制子进程
这个有几个问题需要说明。
父进程在控制子进程的时候,会向子进程派发任务,但是可能父进程在向子进程派发任务的时候,子进程正忙着做其它任务,这个任务的耗费时间久,等到子进程执行完之后,父进程往管道中已经写了好几个任务,由于管道是面向字节流的,所以子进程会一口气将所有的任务全部读出来,这样就不好进行任务的辨别了 。
因此,我们约定,进程在读写时统一读写4字节。
另外父进程在选择子进程执行任务时,需要负载均衡 ,不能总是选择一个进程发放任务。那就有几个策略,包括轮询、随机、和设置权重。轮询就是轮着发放,随机可以设置随机数选择子进程,设置权重可以按照权重来发放任务。这里我们会选择轮询选择子进程的策略。
那么将来还要派发任务,和选择子进程需要的过程一样,我们将会随机选择子进程。
也就是说父进程控制子进程的工作分为三步,第一步是选择一个子进程 ,第二步是选择一个任务 ,第三步是给指定子进程派送指定任务。
2.1 选择一个子进程
我们采用轮询的方式选择。在进程池类中将选择的函数封装为私有。
cpp
class ProcessPool
{
public:
void Run()
{
while(true)
{
// 1. 选择一个 Channel, 管道 + 子进程
int index = SelectProcessIndex();
std::cout << "index: " << index << std::endl;
sleep(1);
// 2. 选择一个任务
// 3. 发放任务给特定的子进程
}
}
private:
int SelectProcessIndex()
{
static int index = 0;
int Selectnum = index;
index++;
index %= channels.size();
return Selectnum;
}
};
cpp
int main()
{
// 0.创建并初始化进程池
ProcessPool pp;
pp.Init(Do_Task);
// 1.父进程控制子进程
pp.Run();
sleep(100); // 让父进程休眠
return 0;
}
运行测试 :

如上图,轮询选择子进程。
2.2 选择一个任务
这里采用随机数的方式选择一个任务。
相关任务列表就直接使用硬编码定义了。
cpp
void SyncDisk()
{
std::cout << getpid() << " -> 刷新数据到磁盘任务....." << std::endl;
sleep(1);
}
void Download()
{
std::cout << getpid() << " -> 下载数据到系统中....." << std::endl;
sleep(1);
}
void PrintLog()
{
std::cout << getpid() << " -> 打印日志到本地....." << std::endl;
sleep(1);
}
void UpdateStatus()
{
std::cout << getpid() << " -> 更新一次用户的状态....." << std::endl;
sleep(1);
}
typedef void (*cb_task)(); // 函数指针
cb_task tasks[4] = {SyncDisk, Download, PrintLog, UpdateStatus}; // 任务列表
然后使用随机数选择任务。使用随机数需要种下随机数种子,这里就种在进程池的构造函数里了。
cpp
class ProcessPool
{
public:
ProcessPool()
{
// 随机数种子
srand((unsigned int)time(nullptr) ^ getpid()); // 让它更随机
}
void Run()
{
while(true)
{
// 1. 选择一个 Channel, 管道 + 子进程
int index = SelectProcessIndex();
// 2. 选择一个任务
int itask = SelectTasks();
std::cout << "index: " << index << " itask: " << itask << std::endl;
sleep(1);
// 3. 发放任务给特定的子进程
}
}
private:
int SelectProcessIndex() // 选择一个进程
{
static int index = 0;
int Selectnum = index;
index++;
index %= channels.size();
return Selectnum;
}
int SelectTasks() // 选择一个任务
{
return rand() % 4;
}
};
cpp
int main()
{
// 0.创建并初始化进程池
ProcessPool pp;
pp.Init(Do_Task);
// pp.DeBug();
// 1.父进程控制子进程
pp.Run();
sleep(100); // 让父进程休眠
return 0;
}
cpp
int main()
{
// 0.创建并初始化进程池
ProcessPool pp;
pp.Init(Do_Task);
// 1.父进程控制子进程
pp.Run();
sleep(100); // 让父进程休眠
return 0;
}
运行测试 :

如上图,轮询选择进程,随机选择任务,符合预期。
2.3 父进程发送任务给子进程
现在选择出了一个任务,并且选择出了要执行任务的进程,所以现在父进程就可以通过管道向子进程派发任务了。相关派送任务的函数可以设置成进程池类的私有成员函数。
cpp
// 通道类,管理匿名管道和读端的子进程
class Channel
{
public:
// ...
void Write(int itask) // 通过管道传递任务给子进程
{
ssize_t n = write(_wfd, &itask, sizeof itask); // 约定父进程一次写 4 字节数据
(void)n; // 防止编译器告警
}
private:
int _wfd; // 写端的文件描述符
pid_t _sub_pid; // 管道读端子进程的 pid
std::string _sub_name; // 关于子进程的描述
};
class ProcessPool
{
public:
// ...
void Run()
{
while(true)
{
std::cout << "------------------------------------" << std::endl;
// 1. 选择一个 Channel, 管道 + 子进程
int index = SelectProcessIndex();
// 2. 选择一个任务
int itask = SelectTasks();
std::cout << "index: " << index << " itask: " << itask << std::endl;
// 3. 发放任务给特定的子进程
std::cout << "派送 " << itask << " 任务给 " << index << " 号子进程" << std::endl;
SendTask2Salver(index, itask);
sleep(1); // 一秒一个任务
}
}
private:
void SendTask2Salver(int index, int itask)
{
if(index < 0 || index >= channels.size())
return;
if(itask < 0 || itask >= 4)
return;
channels[index].Write(itask); // 调用通道类的成员函数,通过管道传递任务给子进程
}
// ...
private:
// 0. 组织Channel的容器
std::vector<Channel> channels;
};
上面的代码中父进程通过进程池类的私有成员函数SendTask2Salver通过管道向子进程写数据,这个函数内部又调用了通道类的成员函数Write向子进程写数据,并且约定父进程一次写入四字节数据。
这样父进程就向子进程写入了数据,紧接着就应该子进程读取数据。子进程的逻辑是创建出来之后,关闭自己的写端,然后转而执行我们给的回调函数cb,在这里就是Do_Task函数,执行完这个函数里面的任务,子进程就退出了。
所以子进程读取数据的代码就发生在Do_Task函数内部。并且它会在这个函数中执行完任务。
cpp
void Do_Task(int fd) // 子进程的入口函数
{
while(true)
{
int task_code = 0; // 存储要执行的任务的编号
ssize_t n = read(fd, &task_code, sizeof task_code); // 约定子进程一次读取 4 字节数据
if(n == sizeof task_code)
{
tasks[task_code](); // 子进程执行任务
}
else if(n == 0) // 父进程关闭了管道的写端
{
std::cout << "子进程:" << getpid() << " 退出..." << std::endl;
break;
}
else
{
std::cerr << "Reading error!" << std::endl;
break;
}
}
}
以上就是子进程读取数据的逻辑,约定子进程一次读取四字节的数据。子进程会通过读取到的数据来执行任务。当父进程关闭写端时,子进程会读到0,也就是read返回0(EOF),此时子进程也应该退出了。
cpp
int main()
{
// 0.创建并初始化进程池
ProcessPool pp;
pp.Init(Do_Task);
// 1.父进程控制子进程
pp.Run();
sleep(100); // 让父进程休眠
return 0;
}
运行测试 :

如上图,通过运行结果,我们看到父进程成功给每个子进程派发了任务,并且子进程执行了父进程派发的任务,这就是主从模式的进程池。
三、释放和回收资源
上面我们的父进程是一直在给子进程派发任务的,那么如果未来是要执行有限次的任务,当派发任务结束并且子进程执行完任务时,父进程也要能够释放管道并且回收子进程。
所以接下来的执行,我就让父进程派发五次任务就结束。
我们在设计子进程时,是当子进程读到0,就会退出回调函数Do_Task,然后子进程要执行的就是exit退出,此时子进程就变成僵尸状态了。
所以我们只需要让父进程关闭写端,此时子进程就会变成僵尸状态。
释放和回收资源要做的工作就是让所有子进程退出 和回收所有子进程 两个步骤。所以这个可以封装在进程池类的Quit函数中。
让所有子进程退出可以遍历所有通道,并且关闭写端,我们的写端_wfd和子进程的_sub_pid都在通道类内,所以关闭写端和回收子进程两个工作,都可以在通道类中进行。
cpp
// 通道类,管理匿名管道和读端的子进程
class Channel
{
public:
// ...
void ClosePipe()
{
close(_wfd); // 父进程关闭写端
}
void Wait()
{
pid_t rid = waitpid(_sub_pid, nullptr, 0);
(void)rid; // 避免编译器告警
std::cout << "回收子进程 " << _sub_pid << " 成功..." << std::endl;
}
private:
int _wfd; // 写端的文件描述符
pid_t _sub_pid; // 管道读端子进程的 pid
std::string _sub_name; // 关于子进程的描述
};
class ProcessPool
{
public:
// ...
void Quit()
{
// 1. 让所有子进程退出
for(auto& ch : channels) // 遍历所有通道并关闭写端
{
ch.ClosePipe();
}
// 2. 回收子进程
for(auto& ch : channels)
{
ch.Wait();
}
}
// ...
private:
// 0. 组织Channel的容器
std::vector<Channel> channels;
};
cpp
int main()
{
// 0.创建并初始化进程池
ProcessPool pp;
pp.Init(Do_Task);
// 1.父进程控制子进程
pp.Run();
// 3.释放管道,回收子进程
pp.Quit();
return 0;
}
运行测试 :

如上图,释放管道并且回收子进程成功。
3.1 一个问题
如果父进程在让子进程退出时,子进程的管道里还有任务没有执行,那么会怎样?
子进程会将管道中的任务执行完成之后,再次调用read读取数据返回0时,直接退出。所以所有已写入的任务都会被处理,不会因为父进程关闭管道而丢失。这正是管道机制的优雅之处。
3.2 解决 Bug
分析出现原因
我们的进程池代码基本写完了,非常的优雅!
但是现在存在一个Bug,当进程池类的Quit函数,像下面这样写时,就会出错。上面我们的Quit函数中包含两个循环,一个循环关闭子进程,另一个循环回收子进程,为什么不边关闭边回收呢? 我们改一下。
cpp
class ProcessPool
{
public:
void Quit()
{
for(auto& ch : channels)
{
ch.ClosePipe(); // 让子进程退出
ch.Wait(); // 回收子进程
}
}
private:
// 0. 组织Channel的容器
std::vector<Channel> channels;
};
运行测试 :

如上图,竟然一个子进程都没退!那么问题出在哪里呢?
我们来分析一下管道的创建过程,父进程首先会创建匿名管道,并且创建子进程,当它创建第一个子进程时,是这样的:

如上,父进程创建第一个进程的时候很顺利,最后父进程是管道的写端,子进程是管道的读端。
那么如果父进程再创建一个子进程呢? 依旧是先创建管道再创建子进程,然后关闭读写端,这里我们直接呈现出最后的效果图。

如上图,这是连续创建两个子进程的情况,发现了吗?子进程会以父进程为模板创建出来,所以父进程管理之前子进程的写端也被复制下来了,所以后来的进程也会有之前子进程管道的写端。
因此最先创建的子进程是最吃亏的,除了父进程有它的管道的写端,其余的所有进程都会有它的管道的写端。
所以没有一个子进程退出的原因就是由于我们是正序遍历的,所以父进程将第一个进程写端关闭之后,其余的所有子进程都还有这个子进程的写端,因此第一个子进程就会阻塞在那里,等待管道的写端发送数据,因此没有一个子进程退出。
所以原因:写端文件描述符wfd没有关闭完毕。
验证,我们要看到后面的子进程的文件描述符确实不止一个,这里我们就让父进程创建出所有子进程后一直休眠,然后我们查询所有子进程的文件描述符。
运行测试 :

如上图,验证完毕,我们发现后面的子进程有的文件描述符的数量越来越多。
解决方案
- 方案一 :逆向回收。
我们发现前面的子进程的写端不只有子进程一个,后面的子进程都会有指向前面的子进程管道的写端。所以最后一个子进程的写端只有父进程一个,我们只要倒着来,就可以成功回收。
cpp
void Quit()
{
// 逆向回收
int end = channels.size() - 1;
while(end >= 0)
{
channels[end].ClosePipe();
channels[end].Wait();
end--;
}
}
运行测试 :

如上,释放所有管道,回收所有子进程成功。
- 方案二 :解决
bug,让每个子进程都只有父进程一个写端。
上面的方案一并没有真正解决bug,只是和之前一样,带着bug回收了子进程。这里我们要解决bug。
回想一下,我们的子进程在创建的时候,可能会有很多多余的指向其它子进程的管道的写端,所以我们的子进程只要关闭历史上父进程创建的写端就好了。
这样就保证了子进程自己只有自己管道的读端,如此一来子进程管道的写端和读端就都只有一个了。
那么历史的父进程的写端在那里保存呢? 当然是在通道类中,我们的通道类中有一个成员就是_wfd,存储每个管道的写端。
所以我们修改创建子进程时的代码,让子进程在创建出来之后,先关闭历史上父进程的写端。
cpp
// 通道类,管理匿名管道和读端的子进程
class Channel
{
public:
// ...
void ClosePipe()
{
close(_wfd); // 当前进程关闭写端
}
// ...
private:
int _wfd; // 写端的文件描述符
pid_t _sub_pid; // 管道读端子进程的 pid
std::string _sub_name; // 关于子进程的描述
};
class ProcessPool
{
public:
// ...
void Init(task_t cb)
{
CreateProcessChannel(cb);
}
// ...
private:
// ...
void CreateProcessChannel(task_t cb) // 初始化进程池,设置任务处理回调
{
// 1. 创建多个管道和多个子进程
for(int i = 1; i <= process_num; i++)
{
// ...
pid_t id = fork(); // 创建子进程
if(id < 0)
{
std::cerr << "fork error!" << std::endl; // 向标准错误写
exit(FORK_ERROR);
}
else if(id == 0)
{
// 子进程关闭历史上父进程管理的管道写端
if(!channels.empty())
{
for(auto& ch : channels)
{
ch.ClosePipe();
}
}
// 子进程
close(pipefd[1]); // 子进程关闭管道写端
// 回调
cb(pipefd[0]); // 子进程通过读端的文件描述符获取任务
exit(OK); // 子进程执行完任务直接退出
}
// ...
}
}
private:
// 0. 组织Channel的容器
std::vector<Channel> channels;
};
上面的代码中,fork创建子进程之后,子进程和父进程的channels就都是旧的了。如果在子进程关闭写端之前,父进程进行了写端的插入,也不用担心,此时父进程由于要修改数据,所以就会发生写时拷贝,然后,父子进程都有各自的channels,也就是说,子进程关闭的永远都是历史父进程创建的写端wfd。
如果还有人不理解,这里以表格呈现:
| 方面 | 说明 |
|---|---|
| 写时拷贝 | 父子进程各自拥有独立的channels副本,互不干扰 |
| 历史写端 | 子进程关闭的是fork时刻存在的写端,不会影响父进程后续添加 |
| 引用计数 | 每个管道的写端最终只被父进程持有,关闭即退出 |
| 内存安全 | 写时拷贝确保进程不会访问彼此的内存空间 |
这次我们就可以正序边释放管道,边回收子进程了。
cpp
void Quit()
{
for(auto& ch : channels)
{
ch.ClosePipe(); // 让子进程退出
ch.Wait(); // 回收子进程
}
// 逆向回收
// int end = channels.size() - 1;
// while(end >= 0)
// {
// channels[end].ClosePipe();
// channels[end].Wait();
// end--;
// }
}
运行测试 :

如上,Bug被修复了,每个子进程的管道都只有一个写端一个读端。
再次验证,让父进程创建完毕之后休眠,我们查询子进程的fd。

如上图,子进程都只有一个读端!父进程掌管着各子进程管道的写端!
到此,进程池代码编写完毕。
四、源代码
cpp
#include <iostream>
#include <string>
#include <vector>
#include <functional>
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
////////////////////子进程要完成的任务//////////////////////////////
void SyncDisk()
{
std::cout << getpid() << " -> 刷新数据到磁盘任务....." << std::endl;
sleep(1);
}
void Download()
{
std::cout << getpid() << " -> 下载数据到系统中....." << std::endl;
sleep(1);
}
void PrintLog()
{
std::cout << getpid() << " -> 打印日志到本地....." << std::endl;
sleep(1);
}
void UpdateStatus()
{
std::cout << getpid() << " -> 更新一次用户的状态....." << std::endl;
sleep(1);
}
typedef void (*cb_task)(); // 函数指针
cb_task tasks[4] = {SyncDisk, Download, PrintLog, UpdateStatus}; // 任务列表
////////////////////进程池相关/////////////////////////////////////
// 自定义退出码含义
enum
{
OK,
PIPE_ERROR,
FORK_ERROR
};
const int process_num = 5;
// 定义任务函数类型 - 带 int 参数
using task_t = std::function<void(int)>;
void Do_Task(int fd) // 子进程的入口函数
{
while(true)
{
int task_code = 0; // 存储要执行的任务的编号
ssize_t n = read(fd, &task_code, sizeof task_code); // 约定子进程一次读取 4 字节数据
if(n == sizeof task_code)
{
tasks[task_code](); // 子进程执行任务
}
else if(n == 0) // 父进程关闭了管道的写端
{
std::cout << "子进程:" << getpid() << " 退出..." << std::endl;
break;
}
else
{
std::cerr << "Reading error!" << std::endl;
break;
}
}
}
// 通道类,管理匿名管道和读端的子进程
class Channel
{
public:
Channel(int wfd, pid_t sub_pid)
:_wfd(wfd)
,_sub_pid(sub_pid)
{
_sub_name = "子进程:" + std::to_string(sub_pid);
}
~Channel()
{}
void PrintInfo()
{
printf("父进程管理:wfd -> %d, pid -> %d, 描述 -> %s\n", _wfd, _sub_pid, _sub_name.c_str());
}
void Write(int itask) // 通过管道传递任务给子进程
{
ssize_t n = write(_wfd, &itask, sizeof itask); // 约定父进程一次写 4 字节数据
(void)n; // 防止编译器告警
}
void ClosePipe()
{
close(_wfd); // 当前进程关闭写端
}
void Wait()
{
pid_t rid = waitpid(_sub_pid, nullptr, 0);
(void)rid; // 避免编译器告警
std::cout << "回收子进程 " << _sub_pid << " 成功..." << std::endl;
}
private:
int _wfd; // 写端的文件描述符
pid_t _sub_pid; // 管道读端子进程的 pid
std::string _sub_name; // 关于子进程的描述
};
class ProcessPool
{
public:
ProcessPool()
{
// 随机数种子
srand((unsigned int)time(nullptr) ^ getpid()); // 让它更随机
}
~ProcessPool()
{}
void Init(task_t cb)
{
CreateProcessChannel(cb);
}
void DeBug()
{
for(auto& c : channels)
{
c.PrintInfo();
}
}
void Run()
{
int cnt = 5;
// while(true)
while(cnt--)
{
std::cout << "------------------------------------" << std::endl;
// 1. 选择一个 Channel, 管道 + 子进程
int index = SelectProcessIndex();
// 2. 选择一个任务
int itask = SelectTasks();
std::cout << "index: " << index << " itask: " << itask << std::endl;
// 3. 发放任务给特定的子进程
std::cout << "派送 " << itask << " 任务给 " << index << " 号子进程" << std::endl;
SendTask2Salver(index, itask);
sleep(1); // 一秒一个任务
}
}
void Quit()
{
for(auto& ch : channels)
{
ch.ClosePipe(); // 让子进程退出
ch.Wait(); // 回收子进程
}
// 逆向回收
// int end = channels.size() - 1;
// while(end >= 0)
// {
// channels[end].ClosePipe();
// channels[end].Wait();
// end--;
// }
// // 1. 让所有子进程退出
// for(auto& ch : channels) // 遍历所有通道并关闭写端
// {
// ch.ClosePipe();
// }
// // 2. 回收子进程
// for(auto& ch : channels)
// {
// ch.Wait();
// }
}
private:
void SendTask2Salver(int index, int itask)
{
if(index < 0 || index >= channels.size())
return;
if(itask < 0 || itask >= 4)
return;
channels[index].Write(itask); // 调用通道类的成员函数,通过管道传递任务给子进程
}
int SelectProcessIndex() // 选择一个进程
{
static int index = 0;
int Selectnum = index;
index++;
index %= channels.size();
return Selectnum;
}
int SelectTasks() // 选择一个任务
{
return rand() % 4;
}
void CreateProcessChannel(task_t cb) // 初始化进程池,设置任务处理回调
{
// 1. 创建多个管道和多个子进程
for(int i = 1; i <= process_num; i++)
{
int pipefd[2];
int n = pipe(pipefd); // 创建管道
if(n < 0)
{
std::cerr << "pipe create error!" << std::endl; // 向标准错误写
exit(PIPE_ERROR);
}
pid_t id = fork(); // 创建子进程
if(id < 0)
{
std::cerr << "fork error!" << std::endl; // 向标准错误写
exit(FORK_ERROR);
}
else if(id == 0)
{
// 子进程关闭历史上父进程管理的管道写端
if(!channels.empty())
{
for(auto& ch : channels)
{
ch.ClosePipe();
}
}
// 子进程
close(pipefd[1]); // 子进程关闭管道写端
// 回调
cb(pipefd[0]); // 子进程通过读端的文件描述符获取任务
exit(OK); // 子进程执行完任务直接退出
}
// 父进程
close(pipefd[0]); // 父进程关闭管道读端
channels.emplace_back(pipefd[1], id); // 管理每个管道和对应的子进程
// 上面的写法更优,作用等价于下面的
// Channel ch(pipefd[1], id);
// channels.push_back(ch); // 管理每个管道和对应的子进程
std::cout << "创建子进程:" << id << " 成功..." << std::endl;
sleep(1); // 间隔 1 秒创建一次
}
}
private:
// 0. 组织Channel的容器
std::vector<Channel> channels;
};
int main()
{
// 0.创建并初始化进程池
ProcessPool pp;
pp.Init(Do_Task);
// pp.DeBug();
// 1.父进程控制子进程
pp.Run();
// sleep(100); // 让父进程休眠
// 3.释放管道,回收子进程
pp.Quit();
return 0;
}
总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~