Linux系统小项目——“主从设计模式”进程池

本期我们就来写一个一个进程控制一群进程的过程的代码,并且由进程池进行优化。

相关的代码已经上传至作者的个人gitee:楼田莉子/Linux学习喜欢的话请点个赞谢谢

目录

"池化"技术

进程池介绍

具体应用场景

业务代码

用内存池优化前

有内存池优化后

源码

Header.h

ProcessPool.h

Task.h

main.cpp

但是写道这里我们就会有几个问题:


"池化"技术

池化技术的核心思想是 "以空间换时间""资源复用"。它预先创建并维护一组可重用的资源单元("池"),当需要时从中快速获取,用完归还而非销毁。

为什么这是"深刻"的设计?

  1. 对抗熵增:在计算系统中,资源的频繁创建和销毁(如进程、线程、连接)是"高熵"行为,会导致系统碎片化、性能抖动和不可预测性。池化通过建立有序的、可预测的资源生命周期管理,对抗这种混乱,是系统稳定性的基石。

  2. 经济模型:将资源的创建/销毁成本"摊销"到多次使用中。这类似于商业中的"固定资产投资"与"按需租赁"的区别。一次性的高额初始化成本被均摊,单次使用成本急剧下降。

  3. 控制与隔离:池作为一个管理边界,允许系统对资源进行统一的配额、监控、健康检查和优雅降级。例如,通过限制池的大小,可以防止系统因资源耗尽而崩溃,实现"熔断"。

常见池化技术

内存池 :替代频繁的 malloc/free,减少内存碎片,提高分配速度。

数据库连接池:管理昂贵的数据库连接,避免频繁建立TCP连接和身份验证。

线程池:管理线程生命周期,应对大量短耗时任务。

进程池介绍

进程池是池化思想在操作系统进程管理层面的应用。它是一个管理者(Master)预先创建或动态维护一组工作者进程(Worker)的模型。也就是"主从设计模式"的进程池

核心原理是采用了主从架构

  • 主进程 (Master):负责任务的接收、分配、调度和池的管理(如启动、销毁、重启Worker)。它不执行具体任务,是"大脑"。

  • 工作者进程 (Worker):从主进程接收任务,执行具体计算,并返回结果。它们是"手足"。

具体应用场景

  1. 科学计算与数值模拟

    • 场景:蒙特卡洛模拟、有限元分析、大规模矩阵运算。

    • 为什么用进程池 :计算是纯CPU密集型,无共享状态。每个Worker可以独立计算一个参数下的结果,最后汇总。Python的 concurrent.futures.ProcessPoolExecutormultiprocessing.Pool 是典型应用。

  2. 数据管道与ETL

    • 场景:从多个源读取数据,进行清洗、转换、聚合,最后加载到数据仓库。

    • 为什么用进程池:某些转换步骤(如复杂解析、加密解密)可能是CPU密集的。可以将数据分片,由不同的Worker进程并行处理不同的数据块。

  3. Web后端服务(特定环节)

    • 场景:一个图像上传接口,需要对图片进行缩略图生成、人脸识别、水印添加等操作。

    • 为什么用进程池:这些图像处理操作是CPU密集且耗时的。使用进程池处理这些后台任务,可以避免阻塞Web服务器的I/O循环,同时利用多核加速处理。通常与消息队列(如Celery + 进程池)结合,实现异步任务队列。

  4. 模型推理服务

    • 场景:部署一个深度学习模型(如TensorFlow/PyTorch模型),需要同时服务多个预测请求。

    • 为什么用进程池:模型加载到内存后,单次推理是CPU/GPU密集型计算。预先启动多个Worker进程,每个进程持有一份加载好的模型,可以并行处理请求,极大提高吞吐量。同时,进程隔离保证了即使一个推理请求导致异常,也不会影响其他服务。

  5. 批量作业调度

    • 场景:每天定时运行一批报表生成、数据备份、日志分析脚本。

    • 为什么用进程池:主调度器将不同的作业任务分发给进程池中的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);
            }
        }
    }

本期关于进程池的项目到这里就结束了,喜欢的请点赞收藏关注,谢谢

封面图自取:

相关推荐
云边散步2 小时前
godot2D游戏教程系列一(9)-终结
学习·游戏·游戏开发
gs801402 小时前
【Xinference实战】解决部署Qwen3/vLLM时遇到的 max_model_len 超限与 Engine Crash 报错
运维·服务器
走粥2 小时前
选项式API与组合式API的区别
开发语言·前端·javascript·vue.js·前端框架
从此不归路2 小时前
Qt5 进阶【7】网络请求与 REST API 实战:QNetworkAccessManager 深度应用
开发语言·c++·qt
changzehai2 小时前
Rust + VSCode + probe-rs搭建stm32-rs嵌入式开发调试环境
vscode·后端·stm32·rust·嵌入式·probe-rs
知数SEO2 小时前
Centos如何安装高版本Python
linux·python·centos
试剂小课堂 Pro2 小时前
mPEG-Silane:mPEG链单端接三乙氧基硅的亲水性硅烷偶联剂
java·c语言·网络·c++·python·tomcat
jrlong2 小时前
DataWhale大模型基础与量化微调task4学习笔记(第 1章:参数高效微调_LoRA 方法详解)
笔记·学习
CCTI_Curran2 小时前
迷你标签打印机做TELEC认证注意事项
运维·服务器·wifi·蓝牙·telec认证·日本认证·无线产品