Linux_进程池

news2024/7/7 19:09:18

目录

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号开始按顺序往下排。

结语 

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

代码随想录——无重叠区间(Leetcode435)

题目链接 贪心 排序 class Solution {public int eraseOverlapIntervals(int[][] intervals) {int res 0;if(intervals.length 1 || intervals.length 0){return res;}// 按左边界排序Arrays.sort(intervals, new Comparator<int[]>() {public int compare(int[] …

面试突击指南:Java基础面试题3

1.介绍下进程和线程的关系 进程:一个独立的正在执行的程序。 线程:一个进程的最基本的执行单位,执行路径。 多进程:在操作系统中,同时运行多个程序。 多进程的好处:可以充分利用CPU,提高CPU的使用率。 多线程:在同一个进程(应用程序)中同时执行多个线程。 多线程…

学习率调度器简明教程

学习率是神经网络训练中最重要的超参数之一&#xff0c;影响学习过程的速度和有效性。学习率过高会导致模型在最小值附近震荡&#xff0c;而学习率过低会导致训练过程非常缓慢甚至停滞。本文直观地介绍了学习率调度程序&#xff0c;它是用于在训练期间调整学习率的技术。 NSDT工…

解决SeaTunnel 2.3.4版本写入S3文件报错问题

在使用Apache SeaTunnel时&#xff0c;我遇到了一个写入S3文件的报错问题。通过深入调试和分析&#xff0c;找到了问题所在&#xff0c;并提出了相应的解决方案。 本文将详细介绍报错情况、参考资料、解决思路以及后续研究方向&#xff0c;希望对大家有帮助&#xff01; 一、…

江门雅图仕职业技术学校领导一行莅临泰迪智能科技参观调研

7月2日&#xff0c;江门雅图仕职业技术学校总校长肖胜阳、校长许昌、办公室主任任志娟等莅临广东泰迪智能科技股份有限公司产教融合实训中心参观调研。泰迪智能科技董事长张良均、副总经理施兴、产品中心总监周东平、校企合作经理吴桂锋进行热情接待&#xff0c;双方就学校专业…

Python用于处理 DNS 查询库之Dnspython 使用详解

概要 Dnspython 是一个开源的 Python 库,专门用于处理 DNS 查询。它被设计为既简单易用又功能强大,可以满足从简单到复杂的各种 DNS 相关需求。无论是进行基础的 DNS 查询还是进行高级的 DNS 服务器管理,dnspython 都能提供相应的功能。 这个库支持包括 A、AAAA、MX、TXT …

汉光联创HGLM2200N黑白激光多功能一体机加粉及常见问题处理

基本参数&#xff1a; 机器型号&#xff1a;HGLM2200N 产品名称&#xff1a;A4黑白激光多功能一体机 基础功能&#xff1a;打印、扫描、复印 打印速度&#xff1a;22页/分钟 纸张输入容量&#xff1a;150-249页 单面支持纸张尺寸&#xff1a;A4、A5、A6 产品尺寸&#x…

拓展欧几里得和裴蜀定理

裴蜀定理&#xff08;或贝祖定理&#xff09;说明了对任何整数a、b和它们的最大公约数d&#xff0c;关于未知数x和y的线性不定方程&#xff08;称为裴蜀等式&#xff09;&#xff1a;若a,b是整数,且gcd(a,b)d&#xff0c;那么对于任意的整数x,y,axby都一定是d的倍数&#xff0c…

pytorch中的contiguous()

官方文档&#xff1a;https://pytorch.org/docs/stable/generated/torch.Tensor.contiguous.html 其描述contiguous为&#xff1a; Returns a contiguous in memory tensor containing the same data as self tensor. If self tensor is already in the specified memory forma…

音乐发行平台无加密开源源码

适用于唱片公司&#xff0c;用于接收物料&#xff0c;下载物料功能&#xff1a;个人或机构认证&#xff0c;上传专辑和歌曲&#xff0c;版税结算环境要求php7.4Nginx 1、导入数据库 2、/inc/conn.php里填写数据库密码等后台路径/admin&#xff08;可自行修改任意入口名称&…

Java中子类继承和方法重写_java重写父类方法参数变了怎么改

public(非私有)private私有()构造方法不能继承不能继承成员变量能继承能继承成员方法能继承不能继承 1.也不能继承父类的有参构造方法,具体看构造函数继承特点 2.私有的成员变量相当于从父类拷贝一份拿过来用的,不能直接用,需要get/set方法 继承特点 继承中 成员变量访问特点:如…

重参数化技巧

Q&#xff1a;标准正态分布 P&#xff1a;预期的分布&#xff08;假设符合正态分布&#xff09; 学习与 - 手推 Diffusion Model (DDPM) 1/3 &#xff1a;数学原理推导_哔哩哔哩_bilibili

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

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

透过 Go 语言探索 Linux 网络通信的本质

大家好&#xff0c;我是码农先森。 前言 各种编程语言百花齐放、百家争鸣&#xff0c;但是 “万变不离其中”。对于网络通信而言&#xff0c;每一种编程语言的实现方式都不一样&#xff1b;但其实&#xff0c;调用的底层逻辑都是一样的。linux 系统底层向上提供了统一的 Sock…

君子签区块链+AI,驱动组织实现高效合同管理、精准风险控制

在传统合同签署的过程中&#xff0c;企业、组织、机构都面临着合同签署与管理的诸多问题和挑战&#xff1a;合同种类繁多、数量庞大导致起草效率低下&#xff1b;管理流程繁琐、权限分散使得审批周期冗长且效率低下&#xff1b;合同签订版本难以精准复核&#xff0c;风险防控更…

大型网站软件系统架构演进过程

在我们的生活中,通常会使用大型网站系统,比如购物网站淘宝,京东,阿里1688;大型搜索引擎网站百度,社交类的如腾讯旗下的微信,QQ及新浪旗下的微博等,他们通常都有一下特点: 高并发、大流量&#xff1a;这些系统必须能够处理成千上万甚至数百万的并发用户请求&#xff0c;以及持续…

深入理解pytest fixture:提升测试的灵活性和可维护性!

在现代软件开发中&#xff0c;测试是保证代码质量的重要环节。pytest作为一个强大的测试框架&#xff0c;以其灵活的fixture系统脱颖而出。本文将详细介绍pytest中的fixture概念&#xff0c;通过具体案例展示其应用&#xff0c;并说明如何利用fixture提高测试的灵活性和可维护性…

CVPR 2024最佳论文:“神兵”的组合器 Generative Image Dynamics

CVPR 2024的最佳论文来自谷歌、美国加州大学圣迭戈分校。两篇都来至于视频生成领域&#xff0c;可见今年外界对视频生成领域关注度很高。今天的这篇是“Generative Image Dynamics”&#xff0c;Google Research发布的。它的研究成果令人震惊&#xff0c;从单张RGB图像生成连续…

VIM介绍

VIM&#xff08;Vi IMproved&#xff09;是一种高度可配置的文本编辑器&#xff0c;用于有效地创建和更改任何类型的文本。它是从 vi 编辑器发展而来的&#xff0c;后者最初是 UNIX 系统上的一个文本编辑器。VIM 以其键盘驱动的界面和强大的文本处理能力而闻名&#xff0c;是许…

【pytorch14】感知机

单层感知机模型 对于单层的感知机&#xff0c;它的激活函数是一个sigmoid 对于符号的定义做一个规范化&#xff0c;输入层每一层进行一个编号 输入是第0层&#xff0c;上标0表示属于输入层&#xff0c;下标0到n表示一共有n个节点(这里严格来说应该是0~n-1&#xff0c;为了书写…