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.ProcessPoolExecutor 或 multiprocessing.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);
            }
        }
    }

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

封面图自取:

相关推荐
律宏阔6 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔6 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴6 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千6 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei376 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei376 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei376 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
codigger6 小时前
ObjectSense:一门千行内核、把可靠性写进骨子里的面向对象脚本语言
开发语言·编程·编程语言
GeW6 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
百万蹄蹄向前冲6 小时前
双端同步!云服务器装最新Node.js v26.10全过程追踪
服务器·人工智能·node.js