本期我们就来写一个一个进程控制一群进程的过程的代码,并且由进程池进行优化。
相关的代码已经上传至作者的个人gitee:楼田莉子/Linux学习喜欢的话请点个赞谢谢
目录
"池化"技术
池化技术的核心思想是 "以空间换时间" 和 "资源复用"。它预先创建并维护一组可重用的资源单元("池"),当需要时从中快速获取,用完归还而非销毁。
为什么这是"深刻"的设计?
-
对抗熵增:在计算系统中,资源的频繁创建和销毁(如进程、线程、连接)是"高熵"行为,会导致系统碎片化、性能抖动和不可预测性。池化通过建立有序的、可预测的资源生命周期管理,对抗这种混乱,是系统稳定性的基石。
-
经济模型:将资源的创建/销毁成本"摊销"到多次使用中。这类似于商业中的"固定资产投资"与"按需租赁"的区别。一次性的高额初始化成本被均摊,单次使用成本急剧下降。
-
控制与隔离:池作为一个管理边界,允许系统对资源进行统一的配额、监控、健康检查和优雅降级。例如,通过限制池的大小,可以防止系统因资源耗尽而崩溃,实现"熔断"。
常见池化技术:
内存池 :替代频繁的 malloc/free,减少内存碎片,提高分配速度。
数据库连接池:管理昂贵的数据库连接,避免频繁建立TCP连接和身份验证。
线程池:管理线程生命周期,应对大量短耗时任务。
进程池介绍
进程池是池化思想在操作系统进程管理层面的应用。它是一个管理者(Master)预先创建或动态维护一组工作者进程(Worker)的模型。也就是"主从设计模式"的进程池
核心原理是采用了主从架构
-
主进程 (Master):负责任务的接收、分配、调度和池的管理(如启动、销毁、重启Worker)。它不执行具体任务,是"大脑"。
-
工作者进程 (Worker):从主进程接收任务,执行具体计算,并返回结果。它们是"手足"。
具体应用场景
-
科学计算与数值模拟:
-
场景:蒙特卡洛模拟、有限元分析、大规模矩阵运算。
-
为什么用进程池 :计算是纯CPU密集型,无共享状态。每个Worker可以独立计算一个参数下的结果,最后汇总。Python的
concurrent.futures.ProcessPoolExecutor或multiprocessing.Pool是典型应用。
-
-
数据管道与ETL:
-
场景:从多个源读取数据,进行清洗、转换、聚合,最后加载到数据仓库。
-
为什么用进程池:某些转换步骤(如复杂解析、加密解密)可能是CPU密集的。可以将数据分片,由不同的Worker进程并行处理不同的数据块。
-
-
Web后端服务(特定环节):
-
场景:一个图像上传接口,需要对图片进行缩略图生成、人脸识别、水印添加等操作。
-
为什么用进程池:这些图像处理操作是CPU密集且耗时的。使用进程池处理这些后台任务,可以避免阻塞Web服务器的I/O循环,同时利用多核加速处理。通常与消息队列(如Celery + 进程池)结合,实现异步任务队列。
-
-
模型推理服务:
-
场景:部署一个深度学习模型(如TensorFlow/PyTorch模型),需要同时服务多个预测请求。
-
为什么用进程池:模型加载到内存后,单次推理是CPU/GPU密集型计算。预先启动多个Worker进程,每个进程持有一份加载好的模型,可以并行处理请求,极大提高吞吐量。同时,进程隔离保证了即使一个推理请求导致异常,也不会影响其他服务。
-
-
批量作业调度:
-
场景:每天定时运行一批报表生成、数据备份、日志分析脚本。
-
为什么用进程池:主调度器将不同的作业任务分发给进程池中的Worker并行执行,缩短整个批处理窗口的时间。
-
业务代码
一个进程控制一群进程的过程大概就像这样的

我们约定统一四个字节读写。
用内存池优化前
cpp
#include <cstdio>
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>
using namespace std;
// 返回值类型枚举
enum { OK = 0, Pipe_Error, Fork_Error };
// 创建的具体进程数
constexpr int gprocessnum = 5;
// 父进程管理通道
class channel
{
private:
int _wfd; // 写文件描述符
pid_t sub_pid; // 子进程pid的值
string sub_name; // 子进程的名称
public:
channel(int wfd, int pid)
{
_wfd = wfd;
sub_pid = pid;
sub_name = "sub-process_" + to_string(sub_pid);
}
void Print()
{
printf("wfd:%d, who:%d, channel name:%s\n", _wfd, sub_pid, sub_name.c_str());
}
~channel()
{
}
};
void Routine(int fd)
{
// 处理具体的业务逻辑
while (1)
{
sleep(1);
}
}
int main()
{
vector<channel> channels; // 存放所有的通道
// 创建多个管道和进程
for (int i = 0; i < gprocessnum; ++i)
{
int pipefd[2] = {0};
int n = pipe(pipefd);
if (n < 0)
{
cerr << "pipe error" << endl;
exit(Pipe_Error);
}
pid_t id = fork();
if (id < 0) // fork失败
{
cerr << "fork error" << endl;
exit(Fork_Error);
}
else if (id == 0) // 子进程
{
close(pipefd[1]); // 关闭写端
Routine(pipefd[0]); // 处理具体的业务逻辑
exit(OK); // 子进程不会执行后续代码,执行完业务逻辑后直接退出
}
else // 父进程
{
// 父进程write端
close(pipefd[0]); // 关闭读端
channel ch(pipefd[1], id);
// pipefd[1]循环一次就消失了
channels.emplace_back(ch);
cout << "创建子进程" << id << "成功" << endl;
sleep(1);
}
}
// 父进程控制子进程
for (auto &ch : channels)
{
ch.Print();
}
sleep(10);
// 释放所有资源
sleep(10);
return 0;
}
有内存池优化后
cpp
#include <cstdio>
#include <iostream>
#include <string>
#include <functional>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>
using namespace std;
// 返回值类型枚举
enum { OK = 0, Pipe_Error, Fork_Error };
// 创建的具体进程数
constexpr int gprocessnum = 5;
// 任务处理函数类型
using task_t =function<void(int)>;
//等价于 typedef function<void(int)> task_t;
// 父进程管理通道
class channel
{
private:
int _wfd; // 写文件描述符
pid_t sub_pid; // 子进程pid的值
string sub_name; // 子进程的名称
public:
channel(int wfd, int pid)
{
_wfd = wfd;
sub_pid = pid;
sub_name = "sub-process_" + to_string(sub_pid);
}
void Print()
{
printf("wfd:%d, who:%d, channel name:%s\n", _wfd, sub_pid, sub_name.c_str());
}
~channel()
{
}
};
void Routine(int fd)
{
// 处理具体的业务逻辑
while (1)
{
sleep(1);
}
}
class ProcessPool
{
private:
vector<channel> channels; // 存放所有的通道
// 创建进程通道
void CreateProcessChannel(task_t cb) // cb: 具体的业务逻辑处理函数,本质上是回调函数
{
for (int i = 0; i < gprocessnum; ++i)
{
int pipefd[2] = {0};
int n = pipe(pipefd);
if (n < 0)
{
cerr << "pipe error" << endl;
exit(Pipe_Error);
}
pid_t id = fork();
if (id < 0) // fork失败
{
cerr << "fork error" << endl;
exit(Fork_Error);
}
else if (id == 0) // 子进程
{
close(pipefd[1]); // 关闭写端
cb(pipefd[0]); //本质上是回调函数
exit(OK); // 子进程不会执行后续代码,执行完业务逻辑后直接退出
}
else // 父进程
{
// 父进程write端
close(pipefd[0]); // 关闭读端
channel ch(pipefd[1], id);
// pipefd[1]循环一次就消失了
channels.emplace_back(ch);
cout << "创建子进程" << id << "成功" << endl;
sleep(1);
}
}
}
public:
ProcessPool() = default;
~ProcessPool()
{}
void Init(task_t cb)
{
CreateProcessChannel();
}
void Debug()
{
for (auto &ch : channels)
{
ch.Print();
}
}
};
int main()
{
// 初始化进程池
ProcessPool pool;
pool.Init(Routine);
// 父进程控制子进程
pool.Debug();
sleep(10);
// 释放所有资源
sleep(10);
return 0;
}
那么接下来我们的父进程如何选择子进程呢?主要是靠着"负载均衡"。
那么我们该如何进行负载均衡呢?主要分为三步:
1、轮询
2、随机
3、权重
那么我们就必须给每一个channel设置权值。
任务:四个字节读写
源码
接下来我们拆分一下业务和进程池
Header.h
cpp
// 公共头文件
#pragma once
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <string>
#include <functional>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>
using namespace std;
ProcessPool.h
cpp
#include"Header.h"
#include"Task.h"
//进程池类
// 返回值类型枚举
enum { OK = 0, Pipe_Error, Fork_Error };
// 创建的具体进程数
constexpr int gprocessnum = 5;
// 任务处理函数类型
using cb_t =function<void(int)>;
//等价于 typedef function<void(int)> task_t;
// 父进程管理通道
class channel
{
private:
int _wfd; // 写文件描述符
pid_t sub_pid; // 子进程pid的值
string sub_name; // 子进程的名称
public:
channel(int wfd, int pid)
{
_wfd = wfd;
sub_pid = pid;
sub_name = "子进程" + to_string(sub_pid);
}
void Print()
{
printf("wfd:%d, who:%d, channel name:%s\n", _wfd, sub_pid, sub_name.c_str());
}
void Write(int taskid)
{
ssize_t m=write(_wfd, &taskid, sizeof(taskid));//约定4个字节写
(void)m;
}
string Name()
{
return sub_name;
}
void ClosePipe()
{
cout<<"关闭"<<sub_name<<"的管道写端!"<<endl;
close(_wfd);
}
void Wait()
{
pid_t id=waitpid(sub_pid,nullptr,0);
cout<<"回收子进程:"<<id<<"成功!"<<endl;
}
~channel()
{
}
};
//子进程任务入口函数
void Routine(int fd)
{
// 处理具体的业务逻辑
while (1)
{
int taskcode=0;
ssize_t n=read(fd, &taskcode, sizeof(taskcode));//约定4个字节读
//子进程需要sleep吗?不需要
if(n==sizeof(taskcode))
{
if(taskcode<0 || taskcode>=4)
{
cerr<<"任务id不合法!"<<endl;
continue;
}
else
{
//执行任务
task[taskcode]();
}
}
else if(n==0)//父进程结束了,子进程也要结束
{
cout<<"父进程已经结束,子进程"<<getpid()<<"也要退出了!"<<endl;
break;
}
else
{
perror("read");
break;
}
sleep(3);
}
}
// 进程池类
class ProcessPool
{
private:
vector<channel> channels; // 存放所有的通道
// 创建进程通道
void CreateProcessChannel(cb_t cb) // cb: 具体的业务逻辑处理函数,本质上是回调函数
{
for (int i = 0; i < gprocessnum; ++i)
{
int pipefd[2] = {0};
int n = pipe(pipefd);
if (n < 0)
{
cerr << "pipe error" << endl;
exit(Pipe_Error);
}
pid_t id = fork();
if (id < 0) // fork失败
{
cerr << "fork error" << endl;
exit(Fork_Error);
}
else if (id == 0) // 子进程
{
//第三种问题2的解决方案,仅供参考
//关闭历史wfd
// if(!channels.empty())
// {
// for(auto &ch:channels)
// {
// ch.ClosePipe();
// }
// }
close(pipefd[1]); // 关闭写端
cb(pipefd[0]); //本质上是回调函数
exit(OK); // 子进程不会执行后续代码,执行完业务逻辑后直接退出
}
else // 父进程
{
// 父进程write端
close(pipefd[0]); // 关闭读端
channel ch(pipefd[1], id);
// pipefd[1]循环一次就消失了
channels.emplace_back(ch);
cout << "创建子进程" << id << "成功" << endl;
sleep(1);
}
}
}
// 发送任务给指定的子进程
void SendTaskServer(int taskid, int who)
{
if(taskid<0 || taskid>=4)
{
cerr<<"任务id不合法!"<<endl;
return ;
}
if(who<0 || who>=channels.size())
{
cerr<<"子进程id不合法!"<<endl;
return ;
}
channels[who].Write(taskid);
}
// 选择一个子进程通道
int SelectChannel()
{
//选择channel的逻辑
static int index = 0;
int size = channels.size();
int select= index;
index++;
index%=size;
return select;
}
// 选择一个任务
int SelectTask()
{
int taskid = rand()%4;
return taskid;
}
public:
ProcessPool()
{
srand((unsigned int)time(nullptr)^getpid());
}
~ProcessPool()
{}
void Init(cb_t cb)
{
CreateProcessChannel(cb);
}
void Debug()
{
for (auto &ch : channels)
{
ch.Print();
}
}
// 父进程控制子进程运行任务
void run()
{
int cnt=10;
while(cnt--)
{
cout<<"---------------------------------------------------------------------------------------------"<<endl;
//选择channel(选择下标)
int who =SelectChannel();
cout<<"选择了子进程:"<<who<<endl;
//选择任务
int taskid = SelectTask();
cout<<"选择了任务:"<<taskid<<endl;
//发送任务给指定channel
printf("发送了%d任务给%s\n",taskid,channels[who].Name().c_str());
SendTaskServer(taskid,who);
sleep(3);
}
}
void Quit()
{
//所有子进程退出
for(auto &ch:channels)
{
ch.ClosePipe();
}
//回收子进程
for(auto& ch:channels)
{
ch.Wait();
}
//父进程退出
}
};
Task.h
cpp
#include"Header.h"
//子进程任务
//函数指针
//意思为:定义一个函数指针类型,命名为 task_t,这个函数指针指向一个返回值为 void、参数列表为空的函数
// typedef void(*task_t)();
using task_t = void(*)(); //更简单易懂的写法
//刷新数据到磁盘
void Snyc()
{
cout<<"子进程:"<<getpid()<<"正在执行刷新数据到磁盘任务"<<endl;
sleep(3);
cout<<"子进程:"<<getpid()<<"同步任务执行完毕"<<endl;
}
//下载数据到系统
void Download()
{
cout<<"子进程:"<<getpid()<<"正在执行下载数据到系统任务"<<endl;
sleep(3);
cout<<"子进程:"<<getpid()<<"下载任务执行完毕"<<endl;
}
//打印日志到本地
void PrintLog()
{
cout<<"子进程:"<<getpid()<<"正在执行打印日志到本地任务"<<endl;
sleep(3);
cout<<"子进程:"<<getpid()<<"打印日志任务执行完毕"<<endl;
}
//更新用户状态
void UpdateUserStatus()
{
cout<<"子进程:"<<getpid()<<"正在执行更新用户状态任务"<<endl;
sleep(3);
cout<<"子进程:"<<getpid()<<"更新用户状态任务执行完毕"<<endl;
}
//任务函数指针数组
task_t task[4]={Snyc,Download,PrintLog,UpdateUserStatus};
main.cpp
cpp
#include"ProcessPool.h"
int main()
{
// 初始化进程池
ProcessPool pool;
pool.Init(Routine);
pool.Debug();
// 父进程控制子进程
pool.run();
sleep(3);
// 释放所有资源
pool.Quit();
sleep(3);
return 0;
}
执行结果为:
bash
loukou-ruizi@lavm-y1ct01xg2a:~/linux-learning/ITC/ProcessPool$ ./ProcessPool
创建子进程196592成功
创建子进程196595成功
创建子进程196606成功
创建子进程196616成功
创建子进程196619成功
wfd:4, who:196592, channel name:子进程196592
wfd:5, who:196595, channel name:子进程196595
wfd:6, who:196606, channel name:子进程196606
wfd:7, who:196616, channel name:子进程196616
wfd:8, who:196619, channel name:子进程196619
---------------------------------------------------------------------------------------------
选择了子进程:0
选择了任务:3
发送了3任务给子进程196592
子进程:196592正在执行更新用户状态任务
---------------------------------------------------------------------------------------------
选择了子进程:1
选择了任务:3
发送了3任务给子进程196595
子进程:196595正在执行更新用户状态任务
子进程:196592更新用户状态任务执行完毕
---------------------------------------------------------------------------------------------
选择了子进程:2
选择了任务:3
发送了3任务给子进程196606
子进程:196595更新用户状态任务执行完毕
子进程:196606正在执行更新用户状态任务
---------------------------------------------------------------------------------------------
选择了子进程:3
选择了任务:3
发送了3任务给子进程196616
子进程:196606更新用户状态任务执行完毕
子进程:196616正在执行更新用户状态任务
---------------------------------------------------------------------------------------------
选择了子进程:4
选择了任务:1
发送了1任务给子进程196619
子进程:196616更新用户状态任务执行完毕
子进程:196619正在执行下载数据到系统任务
---------------------------------------------------------------------------------------------
选择了子进程:0
选择了任务:2
发送了2任务给子进程196592
子进程:196592正在执行打印日志到本地任务
子进程:196619下载任务执行完毕
子进程:196592打印日志任务执行完毕
---------------------------------------------------------------------------------------------
选择了子进程:1
选择了任务:0
发送了0任务给子进程196595
子进程:196595正在执行刷新数据到磁盘任务
---------------------------------------------------------------------------------------------
子进程:196595同步任务执行完毕
选择了子进程:2
选择了任务:3
发送了3任务给子进程196606
子进程:196606正在执行更新用户状态任务
---------------------------------------------------------------------------------------------
选择了子进程:3
选择了任务:2
发送了2任务给子进程196616
子进程:196616正在执行打印日志到本地任务
子进程:196606更新用户状态任务执行完毕
---------------------------------------------------------------------------------------------
子进程:196616打印日志任务执行完毕
选择了子进程:4
选择了任务:0
发送了0任务给子进程196619
子进程:196619正在执行刷新数据到磁盘任务
子进程:196619同步任务执行完毕
关闭子进程196592的管道写端!
关闭子进程196595的管道写端!
关闭子进程196606的管道写端!
关闭子进程196616的管道写端!
关闭子进程196619的管道写端!
父进程已经结束,子进程196619也要退出了!
父进程已经结束,子进程196616也要退出了!
父进程已经结束,子进程196606也要退出了!
父进程已经结束,子进程196595也要退出了!
父进程已经结束,子进程196592也要退出了!
回收子进程:196592成功!
回收子进程:196595成功!
回收子进程:196606成功!
回收子进程:196616成功!
回收子进程:196619成功!
}
但是写道这里我们就会有几个问题:
1、如果父进程要求子进程退出,但是子进程没有完成任务怎么办?
如果父进程写端关闭,子进程会一直到读端结束才会退出。
2、为什么不子进程全部关闭再回收?就像下面这样
cpp
// version2
for (auto &channel : channels) // 为什么不能这样写?应该怎么写?bug?
{
channel.closePipe();
channel.wait();
}
因为这样wfd没有完全关闭,留下了最后一个
所以要解决的方案有两个:
1最后一个子进程wfd只有一个
2逆序回收
cpp
int end=channels.end();
while(end>=0)
{
channels[end].ClosePipe();
channels[end].Wait();
end--;
}
3关闭历史wfd
cpp
// 创建进程通道
void CreateProcessChannel(cb_t cb) // cb: 具体的业务逻辑处理函数,本质上是回调函数
{
for (int i = 0; i < gprocessnum; ++i)
{
int pipefd[2] = {0};
int n = pipe(pipefd);
if (n < 0)
{
cerr << "pipe error" << endl;
exit(Pipe_Error);
}
pid_t id = fork();
if (id < 0) // fork失败
{
cerr << "fork error" << endl;
exit(Fork_Error);
}
else if (id == 0) // 子进程
{
//第三种问题2的解决方案,仅供参考
//关闭历史wfd
if(!channels.empty())
{
for(auto &ch:channels)
{
ch.ClosePipe();
}
}
close(pipefd[1]); // 关闭写端
cb(pipefd[0]); //本质上是回调函数
exit(OK); // 子进程不会执行后续代码,执行完业务逻辑后直接退出
}
else // 父进程
{
// 父进程write端
close(pipefd[0]); // 关闭读端
channel ch(pipefd[1], id);
// pipefd[1]循环一次就消失了
channels.emplace_back(ch);
cout << "创建子进程" << id << "成功" << endl;
sleep(1);
}
}
}
本期关于进程池的项目到这里就结束了,喜欢的请点赞收藏关注,谢谢
封面图自取:
