Linux_进程池

目录

1、进程池基本逻辑

2、实现进程池框架

3、文件描述符的继承

4、分配任务给进程池 

5、让进程池执行任务 

6、回收子进程 

7、进程池总结 

结语 


前言:

        在Linux下,进程池表示把多个子进程用数据结构的方式进行统一管理,在任何时候都可以对进程池里的子进程进行任务发放,即进程池可以实现并发执行流,能够同时执行多个任务,相比于单进程单一执行流,进程池在处理多任务的效率上有了显著提升。

1、进程池基本逻辑

        进程池之所以能够实现多任务的并发执行,是因为进程池本质是进程间通信(即主进程以通信的形式向进程池里的进程发送任务),并且进程池大部分是由父子进程实现的,所以可以使用匿名管道来实现进程池,说到匿名管道就离不开接口pipe,该接口介绍如下:

#include <unistd.h>//pipe所需要的头文件
 
//传一个数组给到pipe,pipe调用成功时返回0,失败返回-1
//调用成功pipefd数组的第一个元素是读的下标,第二个元素是写的下标
int pipe(int pipefd[2]);

         有了此接口就能够搭配fork接口实现父进程与多个子进程的通信了,上面提到进程池是由数据结构对子进程进行管理的,因此我们需要一个数据结构来方便控制子进程,并且可以让父进程通过该数据结构来调度子进程,所以可以定义一个类来描述子进程,代码如下:

class process
{
public:
    process(int id, const string &name, int fd)
        : id_(id), name_(name), fd_(fd)
    {
    }
    ~process()
    {
    }

public:
    pid_t id_;    // 子进程id
    string name_; // 子进程名称
    int fd_;      // 控制子进程的管道写端(真正控制子进程就是写端的文件描述符)
};

        可以理解为创建一个子进程就用process类来描述他,因此创建10个子进程就会有10个process实例化的对象,然后对这10个进行管理即对10个子进程进行管理,可以把这10个对象放到vector内,vector就是管理进程池的数据结构。

        至此有了上面的概念,就可以搭建基本的进程池框架了,但是要注意一点,即父进程只读,子进程只写,因此要关闭对应的文件描述符,因为管道是半双工通信,只能一边写一边读,示意图如下:

        进程池思路:利用for循环10次,循环调用pipe接口和fork接口,这样就能创建十个子进程,并且每个子进程都有一个匿名管道来与父进程进行通信。当父进程关闭了3号文件描述符后,下一次父进程再次调用pipe接口时,依旧是3号为读端,这样一来创建的子进程统一读端都是3号(因为子进程继承父进程的文件描述符,所以子进程继承了3号读端描述符),子进程的一致性就有了。

2、实现进程池框架

        用代码实现进程并简单测试,代码如下:

#include <iostream>
#include <unistd.h>
#include <vector>
#include <error.h>
#include <string>
#include <sys/types.h>

using namespace std;
#define N 10

class process//描述单个进程的类
{
public:
    process(int id, const string &name, int fd)
        : id_(id), name_(name), fd_(fd)
    {
    }
    ~process()
    {
    }

public:
    pid_t id_;    // 子进程id
    string name_; // 子进程名称
    int fd_;      // 控制子进程的管道写端(真正控制子进程就是写端的文件描述符)
};

int main()
{
    vector<process> vpr;//管道子进程的数据结构
    vector<int> oldfd;//记录写端的文件描述符
    for (int i = 0; i < N; i++)//创建十个子进程的进程池
    {
        int pipefd[2];
        int n = pipe(pipefd);//父进程创建管道
        if (n == -1)
        {
            perror("pipe");
            exit(-1);
        }

        // 创建子进程
        pid_t id = fork();
        //子进程执行流
        if (id == 0)
        {
            for(auto num:oldfd) close(num);//关闭新子进程继承父进程之前的写端
            close(pipefd[1]);//因为子进程只负责读数据,所以关闭子进程对管道的写端
            dup2(pipefd[0], 0); // 重定向,这一步只是方便后续的测试
            exit(0);
        }

        // 父进程执行流
        close(pipefd[0]);//父进程只写,因此关闭父进程对管道的读端
        string pro_name = "子进程" + to_string(i);
        vpr.push_back({id, pro_name, pipefd[1]});//把子进程的信息插入到数据结构中
        oldfd.push_back(pipefd[1]);//记录父进程新打开的写端
    }

    for (auto &num : vpr)//验证进程池里的进程
    {
        cout << num.name_ << ":" << num.id_ << ":" << num.fd_ << endl;
    }
    return 0;
}

        运行结果:

        从测试结果可以发现,确实生成了十个子进程,并且可以通过vector找到他们,但是上述代码中多创建了一个vector<int> oldfd,这个vector是做什么的呢? 

3、文件描述符的继承

        我们都知道一个子进程是会继承父进程的PCB结构体的,自然也会继承PCB结构体里的所有内容,而文件描述符就是PCB结构体里的内容之一,所以文件描述符理应被子进程继承,那么在进程池中就会面临这样一个问题:虽然子进程关闭了写端,但是父进程的写端是会越来越多的,而每次创建的子进程只关闭新的写端,会导致新创建的子进程继承了父进程之前打开的写端,并且这些写端没有得到关闭,具体示意图如下:

        所以随着越来越多的管道被创建,后续创建的子进程会有大量的写端被打开,并且他们都是指向前面子进程的管道,因此需要用vector记录每一次父进程新打开的写端,因为这些新打开的写端也会拷贝到子进程中,所以在子进程中遍历该vector就能关闭子进程继承而来的写端,这就是vector<int> oldfd的作用。

4、分配任务给进程池 

        有了上述的进程池框架,接下来就可以对进程池中的每个进程分配任务了,再次之前可以先定义一个任务列表,用函数指针的方式将这些任务用vector管理起来,表示进程池即将处理的任务,任务列表如下: 

typedef void (*task)();
vector<task> vt; // 任务队列

void task1()
{
    cout << "检测当前角色健康状态" << endl;
}

void task2()
{
    cout << "检测当前角色物品补给" << endl;
}

void task3()
{
    cout << "检测当前角色生命值" << endl;
}

void task4()
{
    cout << "检测当前角色法力值" << endl;
}

void creator_task(vector<task> &vt)//把任务插入到任务队列中
{
    vt.push_back(task1);
    vt.push_back(task2);
    vt.push_back(task3);
    vt.push_back(task4);
}

         有了任务列表后父进程就可以分配任务了,因为任务列表本身是一个vector,并且里面存放的是函数指针,因此父进程给子进程传递vector的下标,这个过程就是任务的派发,子进程拿到下标就可以拿到vector的元素并且调用,这个过程就是任务的执行,父进程派发任务的代码如下:

//父进程开始派送任务
    cout<<"主进程开始给子进程分配任务"<<endl;
    sleep(2);

    for (int i = 0; i < 5; i++)
    {
        int proc_num = rand()%N;//随机子进程-vpr下标
        int task_num = rand()%4;//随机任务
        write(vpr[proc_num].fd_,&task_num,sizeof(int));
        //分配任务的核心就是进程间通信
        sleep(1);
    }

        从上述代码中可以发现,让进程池执行任务的本质就是父进程通过调用write函数传递任务列表的下标给到子进程,这就是为什么进程池是通过进程间通信实现的

5、让进程池执行任务 

        执行任务主要是子进程的工作,所以在子进程的执行流中还要添加一个等待任务的动作,因为进程池的本质是进程间通信,所以子进程等待任务的动作就是调用read函数,等父进程往匿名管道中写数据(等待任务就是read函数的阻塞),子进程拿到这些数据就可以执行对应的任务了,下面是子进程等待任务的代码:

void chlid_go()//让子进程执行任务
{
    int task = 0;
    while (true)
    {
        int n = read(0, &task, sizeof(int));//读取的内容就是任务列表的下标
        if (n > 0)
        {
            cout << "处理该任务的进程是:" << getpid() << ":";
            vt[task]();//根据读取到的下标去调用任务列表里的函数指针
        }
        else
            break;
    }
}

        把该函数填写到子进程的执行流中,就能让子进程执行任务了,代码如下:

#include <iostream>
#include <unistd.h>
#include <vector>
#include <error.h>
#include <string>
#include <sys/types.h>
#include <time.h>

using namespace std;
#define N 10

typedef void (*task)();
vector<task> vt; // 任务队列

//自定义任务列表
void task1()
{
    cout << "检测当前角色健康状态" << endl;
}

void task2()
{
    cout << "检测当前角色物品补给" << endl;
}

void task3()
{
    cout << "检测当前角色生命值" << endl;
}

void task4()
{
    cout << "检测当前角色法力值" << endl;
}

void creator_task(vector<task> &vt)
{
    vt.push_back(task1);
    vt.push_back(task2);
    vt.push_back(task3);
    vt.push_back(task4);
}

//子进程执行任务
void chlid_go()
{
    int task = 0;
    while (true)
    {
        int n = read(0, &task, sizeof(int));//读取的内容就是任务列表的下标
        if (n > 0)
        {
            cout << "处理该任务的进程是:" << getpid() << ":";
            vt[task]();//根据读取到的下标去调用任务列表里的函数指针
        }
        else
            break;
    }
}

class process//描述单个进程的类
{
public:
    process(int id, const string &name, int fd)
        : id_(id), name_(name), fd_(fd)
    {
    }
    ~process()
    {
    }

public:
    pid_t id_;    // 子进程id
    string name_; // 子进程名称
    int fd_;      // 控制子进程的管道写端(真正控制子进程就是写端的文件描述符)
};

int main()
{
    creator_task(vt);
    srand(time(0));
    vector<process> vpr;//管道子进程的数据结构
    vector<int> oldfd;//记录写端的文件描述符
    for (int i = 0; i < N; i++)//创建十个子进程的进程池
    {
        int pipefd[2];
        int n = pipe(pipefd);//父进程创建管道
        if (n == -1)
        {
            perror("pipe");
            exit(-1);
        }

        // 创建子进程
        pid_t id = fork();
        //子进程执行流
        if (id == 0)
        {
            for(auto num:oldfd) close(num);//关闭新子进程继承父进程之前的写端
            close(pipefd[1]);//因为子进程只负责读数据,所以关闭子进程对管道的写端
            dup2(pipefd[0], 0); // 重定向,这一步只是方便后续的测试
            chlid_go();
            exit(0);
        }

        // 父进程执行流
        close(pipefd[0]);//父进程只写,因此关闭父进程对管道的读端
        string pro_name = "子进程" + to_string(i);
        vpr.push_back({id, pro_name, pipefd[1]});//把子进程的信息插入到数据结构中
        oldfd.push_back(pipefd[1]);//记录父进程新打开的写端
    }

    for (auto &num : vpr)//验证进程池里的进程
    {
        cout << num.name_ << ":" << num.id_ << ":" << num.fd_ << endl;
    }

    //父进程开始派送任务
    cout<<"主进程开始给子进程分配任务"<<endl;
    sleep(2);

    for (int i = 0; i < 5; i++)
    {
        int proc_num = rand()%N;//随机子进程-vpr下标
        int task_num = rand()%4;//随机任务
        write(vpr[proc_num].fd_,&task_num,sizeof(int));//分配任务的核心就是进程间通信
        sleep(1);
    }

    return 0;
}

        运行结果:

        至此就完成了给进程池里的进程随机派发任务的实现。 

6、回收子进程 

        上述代码结束后没有对子进程做任何的等待工作,但是结果也是正确的,原因就是当父进程退出后,会关闭父进程所有对匿名管道的写端,写端一关闭,则匿名管道的读端就会读到文件末尾,因此read会返回0,在上面代码中当read返回0时就会跳出循环,从而继续往下执行直到exit退出当前子进程,所以父进程不等待子进程则也不会导致孤儿进程问题(因为父进程退出则子进程一定也会退出)。

        但是为了保证代码、逻辑的完整性,最好还是写一个专门关闭写端和等待子进程的函数加到上述代码的末尾处,代码如下:

for (int i = 0; i < vpr.size(); i++)
    {
        close(vpr[i].fd_);//关闭父进程写端
        waitpid(vpr[i].id_, nullptr, 0);//等待关闭写端的对应子进程
        cout<<"等待子进程:"<<vpr[i].name_<<endl;
        sleep(1);
    }

        测试结果:

7、进程池总结 

        1、进程池通过匿名管道进行父子进程通信而实现的。

        2、进程池控制子进程的策略是通过父进程对匿名管道的写端文件描述符,一个写端对应一个子进程。

        3、注意文件描述符继承的问题,从逻辑上来说子进程要关闭继承父进程的写端文件描述符,即一个子进程只留下对应匿名管道的读端,而父进程要关闭自己的读端。 

        4、默认无其他文件描述符,则子进程的读端始终是3号(因为父进程每次都会关闭3号,导致下一次pipe还是3号为读端),并且所有子进程的读端文件描述符是一样的,父进程的写端从4号开始按顺序往下排。

结语 

        以上就是关于进程池的实现与讲解,进程池允许并发式的执行任务,因此常用进程池处理多任务的场景,并且进程池传递任务和处理任务时就是通过匿名管道传递信息,然后子进程对该信息做解释以达到处理任务的效果。

        最后如果本文有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/772210.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MATLAB和Python发那科ABB库卡史陶比尔工业机器人模拟示教框架

&#x1f3af;要点 &#x1f3af;模拟工业机器人 | &#x1f3af;可视化机器人DH 参数&#xff0c;机器人三维视图 | &#x1f3af;绘制观察运动时关节坐标位置、速度和加速度 | &#x1f3af;绘制每个关节处的扭矩和力 | &#x1f3af;图形界面示教机器人 | &#x1f3af;工业…

通过9大步骤,帮助企业在数字化转型中搭建数据分析的报表体系!

引言&#xff1a;在数字化转型中&#xff0c;企业搭建数据分析的报表体系是一个系统性的过程&#xff0c;需要综合考虑业务需求、数据来源、技术平台等多个方面。此外从报表生命周期的角度来说&#xff0c;从产生、使用以及最后消亡退出体系&#xff0c;都需要通盘考虑&#xf…

[数据集][目标检测]轮椅检测数据集VOC+YOLO格式13826张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;13826 标注数量(xml文件个数)&#xff1a;13826 标注数量(txt文件个数)&#xff1a;13826 标…

remix测试文件测试智能合约

remix内其实也是可以通过编写测试文件来测试智能合约的&#xff0c;需要使用插件自动生成框架以及测试结果。本文介绍一个简单的HelloWorld合约来讲解 安装插件多重检测&#xff1a; &#xff08;solidity unit testing&#xff09; 编译部署HelloWorld合约 // SPDX-License-…

在线图片转文字的软件,分享3种强大的软件!

在信息爆炸的时代&#xff0c;图片作为信息的重要载体之一&#xff0c;其内容往往蕴含着巨大的价值。然而&#xff0c;面对海量的图片信息&#xff0c;如何高效、准确地将其转化为文字&#xff0c;成为了许多人的迫切需求。今天&#xff0c;就为大家盘点几款功能强大的在线图片…

Xilinx FPGA:vivado关于RAM的一些零碎的小知识

一、xilinx fpga嵌入式存储单元 RAM----随机存取存储器&#xff1a;上电工作时可以随时从任何一个指定的地址写入&#xff08;存入&#xff09;或读出&#xff08;取出&#xff09;信息。缺点是一旦断电所存储的数据将随之丢失。RAM在计算机和数字系统中用来暂时性存储程序、数…

腾讯云COS分布式对象存储

腾讯云COS分布式对象存储 腾讯云对象存储&#xff08;Cloud Object Storage&#xff0c;COS&#xff09;是腾讯云提供的一种用于存储海量文件的分布式存储服务。 腾讯云 COS 适用于多种场景&#xff0c;如静态网站托管、大规模数据备份和归档、多媒体存储和处理、移动应用数据存…

【test】小爱同学通过esp32控制电脑开关

文章目录 一、环境准备二、开关机原理数据传输框架 三、环境搭建1.巴法云平台设置2.米家设置3.windows网络唤醒设置4.搭建esp32开发环境并部署&#xff08;1&#xff09;新建项目&#xff08;2&#xff09;导入esp32库&#xff08;3&#xff09; 添加库&#xff08;4&#xff0…

YOLOv8入门 | 重要性能衡量指标、训练结果评价及分析及影响mAP的因素【发论文关注的指标】

秋招面试专栏推荐 &#xff1a;深度学习算法工程师面试问题总结【百面算法工程师】——点击即可跳转 &#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 专栏目录 &#xff1a;《YOLOv8改进有效…

从CVPR 2024看 NeRF 最新改进&应用

三维重建领域必不可少的NeRF技术最近又有新突破了&#xff01; 首先是SAX-NeRF框架&#xff0c;专为稀疏视角下X光三维重建设计&#xff0c;无需CT数据进行训练&#xff0c;只使用 X 光片即可&#xff0c;等于给NeRF开透视眼&#xff01; 还有清华提出的GenN2N&#xff0c;一…

Canvas合集更更更之实现由画布中心向外随机不断发散的粒子效果

实现效果 1.支持颜色设置 2.支持粒子数量设置 3.支持粒子大小设置 写在最后&#x1f352; 源码&#xff0c;关注&#x1f365;苏苏的bug&#xff0c;&#x1f361;苏苏的github&#xff0c;&#x1f36a;苏苏的码云

VSCode 自动调整格式失效了 ESLint

ESLint【最新注意2.4.4版本有问题&#xff0c;需退回2.4.2版本就恢复正常了】 参考&#xff1a;vscode自动格式化失效_vscode保存自动格式化失效-CSDN博客

【启明智显分享】手持遥控器HMI解决方案:2.8寸触摸串口屏助力实现智能化

现代生活不少家居不断智能化&#xff0c;但是遥控器却并没有随之升级。在遥控交互上&#xff0c;传统遥控器明显功能不足&#xff1a;特别是大屏智能电视&#xff0c;其功能主要由各种APP程序实现。在电脑上鼠标轻轻点击、在手机上触摸屏丝滑滑动&#xff0c;但是在电视上这些A…

新的超好用的baas服务他来了!

新的超好用的BaaS服务它来了&#xff01; 你是否厌倦了搭建服务的繁琐过程&#xff1f;你是否因为接口API的开发而头疼不已&#xff1f;你是否梦想着能够用最少的精力打造出最棒的应用&#xff1f;如果你的答案是“是”&#xff0c;那么恭喜你&#xff0c;你的救星来了&#x…

kubernetes dashboard安装

1.查看符合自己版本的kubernetes Dashboard 比如我使用的是1.23.0版本 https://github.com/kubernetes/dashboard/releases?page5 对应版本 kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.5.1/aio/deploy/recommended.yaml修改对应的yaml,…

秋招突击——设计模式补充——单例模式、依赖倒转原则、工厂方法模式

文章目录 引言正文依赖倒转原则工厂方法模式工厂模式的实现简单工厂和工厂方法的对比 抽线工厂模式最基本的数据访问程序使用工厂模式实现数据库的访问使用抽象工厂模式的数据访问程序抽象工厂模式的优点和缺点使用反射抽象工厂的数据访问程序使用反射配置文件实现数据访问程序…

2024亚太杯中文赛数学建模选题建议及各题思路来啦!

大家好呀&#xff0c;2024年第十四届APMCM亚太地区大学生数学建模竞赛&#xff08;中文赛项&#xff09;开始了&#xff0c;来说一下初步的选题建议吧&#xff1a; 首先定下主基调&#xff0c; 本次亚太杯推荐大家选择B题目。C题目难度较高&#xff0c;只建议用过kaiwu的队伍…

决策树算法的原理与案例实现

一、绪论 1.1 决策树算法的背景介绍 1.2 研究决策树算法的意义 二、决策树算法原理 2.1 决策树的基本概念 2.2 决策树构建的基本思路 2.2 决策树的构建过程 2.3 决策树的剪枝策略 三、决策树算法的优缺点 3.1 决策树算法的优势 3.2 决策树算法的局限性 3.3 决策树算…

微服务粒度难题:找到合适的微服务大小

序言 在微服务架构风格中&#xff0c;微服务通常设计遵循SRP&#xff08;单一职责原则&#xff09;&#xff0c;作为一个独立部署的软件单元&#xff0c;专注于做一件事&#xff0c;并且做到极致。作为开发人员&#xff0c;我们常常倾向于在没有考虑为什么的情况下尽可能地将服…

全面教程:在Ubuntu上快速部署ZeroTier,实现Windows与VSCode的局域网无缝访问

文章目录 1 背景介绍2 Windows上的操作3 Ubuntu上的操作4 连接 1 背景介绍 在现代工作环境中&#xff0c;远程访问公司内网的Ubuntu主机对于开发者来说是一项基本需求。然而&#xff0c;由于内网的限制&#xff0c;传统的远程控制软件如向日葵和todesk往往无法满足这一需求。作…