Linux | 进程池技术解析:利用无名管道实现并发任务处理(含实现代码)

在血海里游泳,一直游到海水变蓝。------何小鹏

2024.8.31

目录

一、进程池

二、使用匿名管道实现进程池的核心

前置知识:管道的四种情况和五个特征

三、代码实现

四、实现代码详解

main()

2、loadTask()

3、channelInit()

问题:为什么要将子进程的管道读端重定向至标准输入?

4、ctrlProcess()

5、channelClose()


一、进程池

多任务处理是提高系统性能和响应速度的关键。进程池技术作为一种有效的资源管理和任务调度策略,已经成为并发编程中不可或缺的一部分。本文探讨进程池的工作原理,以及如何通过使用无名管道来实现高效的并发任务处理。

  • 定义:进程池由一组预先创建的空闲进程(资源进程)和管理这些进程的管理进程组成。
  • 作用:优化资源管理和提高系统效率,通过预先创建进程减少频繁创建和销毁进程的开销。
  • 预期并发效果:虽然进程池中的进程数量固定,但可以并行处理多个任务,实现并发效果。
二、使用匿名管道实现进程池的核心
  • 主函数逻辑:创建任务,创建子进程池,发送任务,关闭写端和回收子进程。
  • 父进程创建进程池 :使用fork()函数创建子进程,子进程阻塞在read()函数处等待任务。
  • 子进程退出时机 :当所有管道写端关闭时,子进程通过read()函数返回0值判断退出。
前置知识:管道的四种情况和五个特征

管道的四种情况:

  1. 如果管道是空的,则读取端被阻塞
  2. 如果管道是满的,则写入端被阻塞
  3. 如果关闭了管道的读端,那管道没必要存在,被13号信号杀死
  4. 如果关闭了管道的写端,读取完毕后管道返回0,表示读到了文件末尾

管道的五个特征:

  1. 匿名管道只能用于有血缘关系的进程通信,常用于父子间通信。
  2. 管道内部实现了同步机制,读写具有明显的顺序性。
  3. 管道的生命周期是随进程的,随着进程使用管道而创建缓冲区, 随进程的退出而释放销毁。
  4. 管道通通信是面向字节流的,读写次数是可以不匹配的,读到的数据可能是单次残缺的,也可能是多次堆积的
  5. 管道通信是特殊的半双工模式,半双工是指支持读写,但不能同时读写,特殊在只支持信息的单向传递。
三、代码实现
cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<vector>
#include<string>
#include<sys/wait.h>
#include<sys/types.h>

// 定义任务函数指针类型
typedef void (*task_t)();
#define taskNum 3 // 定义任务数量
int n; // 进程数量
task_t tasks[taskNum]; // 任务函数指针数组
struct channel
{
    int _wfd; // 管道写端文件描述符
    pid_t _task; // 子进程ID
    std::string _name; // 通道名称

    // 构造函数
    channel(int wfd, pid_t task, const std::string& name = "channel")
    :_wfd(wfd),
    _task(task),
    _name(name)
    {}

    // 关闭管道写端
    void Close()
    {
        close(_wfd);
    }

};
std::vector<channel> v; // 通道向量

// 打印任务
void print()
{
    std::cout << "this is print task" << std::endl;
}

// 下载任务
void download()
{
    std::cout << "this is download task" << std::endl;
}

// 刷新任务
void flush()
{
    std::cout << "this is flush task" << std::endl;
}

// 加载任务
void loadTask()
{
    tasks[0] = print;
    tasks[1] = download;
    tasks[2] = flush;
}

// 执行任务
void ExecuteTask()
{
    int read_num = 0;
    int task_index = 0;
    while (true)
    {
        read_num = read(0, &task_index, sizeof(int));
        if (read_num == -1)
        {
            std::cerr << "管道读取失败!错误码:" << errno << std::endl;
            exit(-1);
        }
        else if (read_num == 0) // 读到0代表写端关闭,直接停止
        {
            std::cout << "子进程 " << getpid() << " 任务读取完成或管道关闭,退出。" << std::endl;
            exit(0);
        }
        else
        {
            if (task_index >= 0 && task_index < 3)
            {
                std::cout << "子进程 " << getpid() << " 执行任务 " << task_index << std::endl;
                tasks[task_index]();
            }
            else
            {
                std::cerr << "无效的任务索引:" << task_index << std::endl;
            }
        }
    }
}

// 初始化通道
void channelInit()
{
    for(int i = 0; i < n; i++)
    {
        int pipefd[2];
        pipe(pipefd);
        pid_t id = fork();
        if(id < 0)
            return;
        else if(id == 0)
        {
            close(pipefd[1]);
            dup2(pipefd[0], 0);
            for(int i = 0; i < v.size(); i++)
            {
                v[i].Close();
            }
            ExecuteTask();
        }
        std::string channel_name = "channel_"+std::to_string(i);
        close(pipefd[0]);
        v.push_back(channel(pipefd[1], id, channel_name));
    }
}

// 获取下一个通道索引
int nextChannel()
{
    static int next = 0;
    int channel = next;
    next++;
    next %= n;
    return channel;
}

// 向通道发送任务命令
void sendTaskCommand(channel& chan, int taskCommand)
{
    write(chan._wfd, &taskCommand, sizeof(taskCommand));
}

// 选择任务
int selectTask()
{
    return rand() % taskNum;
}

// 控制进程执行一次
void ctrlProcessOnce()
{
    int taskCommand = selectTask();
    int channel_index = nextChannel();
    sendTaskCommand(v[channel_index], taskCommand);
}

// 控制进程执行多次
void ctrlProcess(int times = -1)
{
    while(times--)
    {
        ctrlProcessOnce();
        sleep(1);
    }
}

// 关闭通道并等待子进程退出
void channelClose()
{
    int status = 0;
    for(int i = 0; i < n; i++)
    {
        v[i].Close();
        wait(&status);
        int exit_status = (status&0x7f);
        int exit_code = ((status>>8)&0xff);
        std::cout << "子进程 " << getpid() << " 的退出状态为 " << exit_status << " 退出码为 " << exit_code << std::endl;
        std::cout << std::endl;
    }
}

// 主函数
int main()
{   
    loadTask();
    std::cout << "请输入你要创建的进程个数"<<std::endl;
    std::cin >> n;
    int count = -1;
    std::cout << "请输入要执行程序的次数" << std::endl;
    std::cin >> count;
    channelInit();
    ctrlProcess(count);
    channelClose();

    sleep(3);
    return 0;
}
四、实现代码详解
main()

负责执行初始化任务、创建进程池、控制进程执行任务,以及关闭通道并等待子进程退出。

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<vector>
#include<string>
#include<sys/types.h>

typedef void (*task_t)();
#define taskNum 3
int n;
task_t tasks[taskNum];
struct channel
{
    int _wfd;
    pid_t _task;
    std::string _name;

    channel(int wfd, pid_t task, const std::string& name = "channel")
    :_wfd(wfd),
    _task(task),
    _name(name)
    {}
    
    void Close()
    {
        close(_wfd);
    }

};
std::vector<channel> v; // 存储创建的子进程信息,包括读wid/进程id/进程名称

// 需要执行的三个函数
void print()
{
    std::cout << "this is print task" << std::endl;
}

void download()
{
    std::cout << "this is download task" << std::endl;
}

void flush()
{
    std::cout << "this is flush task" << std::endl;
}


int main()
{   
    std::cout << "请输入你要创建的进程个数"<<std::endl;
    std::cin >> n;

    loadTask(); // 通过函数指针解耦调用函数,将需要运行的函数放入函数指针数组中
    channelInit();
    ctrlProcess();

    return 0;
}
2、loadTask()

将函数通过函数指针将不同的任务函数加载到数组中,以便后续可以通过索引来调用相应的任务

cpp 复制代码
// 通过函数指针解耦调用函数,将需要运行的函数放入函数指针数组中
void loadTask()
{
    tasks[0] = print;
    tasks[1] = download;
    tasks[2] = flush;
}
3、channelInit()

创建子进程并让子进程read阻塞等待任务。子进程通过管道的读端来接收任务索引,并执行相应的任务函数。

cpp 复制代码
void channelInit()
{
    for(int i = 0; i < n; i++)
    {
        int pipefd[2];
        pipe(pipefd);
        pid_t id = fork();
        if(id < 0)
            return ;
        else if(id == 0)
        {
            close(pipefd[1]);
            dup2(pipefd[0], 0); // 为什么要重定向?------实现监听通道和子进程的解耦
        // 重定向后只需要固定监听0号文件描述符收到的文件即可,无需根据特定子进程监听不同的wid
            for(int i = 0; i < v.size(); i++)
            {
                v[i].Close(); //子进程必须关闭继承父进程的、多余的wfd
            }
            ExecuteTask(); // 子进程在此阻塞等待
        }
        std::string channel_name = "channel_"+std::to_string(i);
        close(pipefd[0]);
        v.push_back(channel(pipefd[1], id, channel_name));
    }
}

// 监听被重定向的wid 获得函数指针数组下标,执行相应函数
void ExecuteTask()
{
    int read_num = 0;
    int task_index = 0;
    while (true)
    {
        read_num = read(0, &task_index, sizeof(int));
        if (read_num == -1)
        {
            std::cerr << "管道读取失败!错误码:" << errno << std::endl;
            exit(-1);
        }
        else if (read_num == 0) // 读到0代表写端关闭,直接停止
        {
            std::cout << "子进程 " << getpid() << " 任务读取完成或管道关闭,退出。" << std::endl;
            break;
        }
        else
        {
            if (task_index >= 0 && task_index < 3)
            {
                std::cout << "子进程 " << getpid() << " 执行任务 " << task_index << std::endl;
                tasks[task_index]();
            }
            else
            {
                std::cerr << "无效的任务索引:" << task_index << std::endl;
            }
        }
    }
}
问题:为什么要将子进程的管道读端重定向至标准输入?

**实现监听通道和子进程的解耦。**每个子进程的读端rid不同,后续read监听需要根据不同子进程调整rid。而重定向后只需要固定监听0号文件描述符收到的文件即可,实现了不同子进程监听同一个文件描述符的解耦。

4、ctrlProcess()

父进程根据轮询策略向子进程发送任务,控制子进程执行任务。

cpp 复制代码
// 获取下一个通道索引
int nextChannel()
{
    static int next = 0;
    int channel = next;
    next++;
    next %= n;
    return channel;
}

// 向通道发送任务命令
void sendTaskCommand(channel& chan, int taskCommand)
{
    write(chan._wfd, &taskCommand, sizeof(taskCommand));
}

// 选择任务
int selectTask()
{
    return rand() % taskNum;
}

// 控制进程执行一次
void ctrlProcessOnce()
{
    int taskCommand = selectTask();
    int channel_index = nextChannel();
    sendTaskCommand(v[channel_index], taskCommand);
}

// 控制进程执行多次
void ctrlProcess(int times = -1)
{
    while(times--)
    {
        ctrlProcessOnce();
        sleep(1);
    }
}
5、channelClose()

用于关闭所有通道,并等待所有子进程退出,同时获取子进程的退出状态和退出码。

cpp 复制代码
// 关闭通道并等待子进程退出
void channelClose()
{
    int status = 0;
    for(int i = 0; i < n; i++)
    {
        v[i].Close();
        wait(&status);
        int exit_status = (status&0x7f);
        int exit_code = ((status>>8)&0xff);
        std::cout << "子进程 " << getpid() << " 的退出状态为 " << exit_status << " 退出码为 " << exit_code << std::endl;
        std::cout << std::endl;
    }
}
相关推荐
ccubee13 分钟前
docker 安装 ftp
运维·docker·容器
咒法师无翅鱼15 分钟前
【定理证明工具调研】Coq, Isabelle and Lean.
算法
枫叶红花36 分钟前
【Linux系统编程】:信号(2)——信号的产生
linux·运维·服务器
风清云淡_A41 分钟前
【java基础系列】实现数字的首位交换算法
java·算法
涵涵子RUSH44 分钟前
合并K个升序链表(最优解)
算法·leetcode
yaosheng_VALVE44 分钟前
探究全金属硬密封蝶阀的奥秘-耀圣控制
运维·eclipse·自动化·pyqt·1024程序员节
_微风轻起1 小时前
linux下网络编程socket&select&epoll的底层实现原理
linux·网络
dami_king1 小时前
SSH特性|组成|SSH是什么?
运维·ssh·1024程序员节
爱吃西瓜的小菜鸡1 小时前
【C语言】矩阵乘法
c语言·学习·算法
启明真纳1 小时前
elasticache备份
运维·elasticsearch·云原生·kubernetes