Linux--进程池

这次,我们来写一个线程池的非常简陋代码,帮助我们理解

我们之前也讲到过:关于C++的池化技术。

进程池是预先创建一组子进程,统一管理任务分配与资源回收的并发模型

什么是进程池?

进程池是预先创建一组空闲进程,统一管理任务队列,避免频繁创建/销毁线程开销的并发编程模型

思路是:

利用父进程管理所有的子进程,而让子进程去完成某种任务!

我们通过匿名管道来实现:

父进程传输信息给子进程,子进程根据父进程传输的信息来执行不同的任务!

简单来说就是:父进程发出指令,由子进程去执行

为什么进程池能够提高效率?

因为OS不相信用户,因此,我们的执行任务的代码中,必定会由大量的系统调用来构成,而上几篇我们讲到,它调用是有成本的!

现在我们采用:预先创建进程,要用时直接拿,而不是需要用到是再去创建进程。同时,在执行完一个任务的子进程还可以继续去执行另外的任务,从而用重复利用的方法提高了资源效率。

我们知道:管理好事物的本质就是:“先描述,在组织!”

创建对象结构体:

class channel
{
public:
    channel(int comfd, pid_t slaverid, std::string processname)
        : _comdfd(comfd), _slaverid(slaverid), _processname(processname)
    {
    }
    // private:
public:
    int _comdfd;              // 发送任务的文件描述符
    pid_t _slaverid;          // 子进程的pid
    std::string _processname; // 子进程的名字,方便打日志
};

我们操作流程主要有:

初始化

控制进程

关闭进程

int main()
{
    //模拟执行任务
    LoadTask(&tasks);
    //种下随机数种子
    srand(time(nullptr));
    // 再组织
    std::vector<channel> channels;
    // 初始化,创建多个进程,建立框架!
    InitSliver(&channels);
    // Debug(channels);
    // std::cout<<channels[0]._comdfd<<":"<<channels[0]._slaverid<<":"<<channels[0]._processname<<std::endl;
    //控制进程
    ctrlSlaver(channels);
    QuitSlaver(channels);
    return 0;
}

模拟执行任务:

#pragma once
#include<iostream>
#include<vector>

//自定义函数指针
typedef void(*task_t)();

void Task1()
{
    std::cout<<"检查单词是否有问题"<<std::endl;
}

void Task2()
{
    std::cout<<"检查是否要更新版本"<<std::endl;
}

void Task3()
{
    std::cout<<"查看下载进度"<<std::endl;
}

void Task4()
{
    std::cout<<"查看用户是否需要登录"<<std::endl;
}

void LoadTask(std::vector<task_t>* tasks)
{
    tasks->push_back(Task1);
    tasks->push_back(Task2);
    tasks->push_back(Task3);
    tasks->push_back(Task4);

进程执行任务

这里通过父进程发送cmdcode,子进程读取它是对应哪个任务,去调用自定义的函数指针的任务。

void sliver()
{
    while(true)
    {
        int cmdcode=0;
        //读取到cmdcode中
        int n=read(0,&cmdcode,sizeof(int));
        if(n==sizeof(int))
        {
            std::cout<<"slaver get a command: "<<getpid()<<" comdcode:"<<cmdcode<<std::endl;
            if(cmdcode>=0 &&cmdcode<tasks.size())
            {
                tasks[cmdcode]();
            }
        }
        if(n==0)
            break;
    }
}

初始化部分

在此之前,虽然我们已经创建好了进程池需要的对象了,但是我们并没有具体开出空间,即里面还没有进程!因此,我们得先进行初始化,提前创建好进程,放入进程池中!

我们通过建管道使得父子进程之间能够建立出联系:

建立管道

创建多进程(子进程执行任务,父进程执行指令)

我们在上面就讲到过,管道是单向通信的,所以需要关闭其中的一个文件描述符。

这里我们为什么用到dup2?

因为这里是通过自己输入要执行的指令是几号任务,所以用到输出重定向。

//输出型参数 *
//输入型参数const &
//输入输出型参数 &
void InitSliver(std::vector<channel>* channels)
{
    //确保每一个子进程都只有一个写端
    std::vector<int>oldfd;
    for (int i = 0; i < 10; i++)
    {
        int pipefd[2];
        // 建立管道
        int n = pipe(pipefd);
        if (n < 0)
        {
            perror("pipe");
            return;
        }
        // 创建父子进程
        pid_t id = fork();
        if (id < 0)
        {
            perror("fork");
            return;
        }
        else if (id == 0) // child
        {
            for(auto&fd:oldfd)
                close(fd);
            close(pipefd[1]); // 关闭一端
            dup2(pipefd[0], 0);
         
            close(pipefd[0]);
            sliver();
            std::cout<<"process: "<<getpid()<<" quit"<<std::endl;
            exit(0);
            
        }
        // father
        
            close(pipefd[0]); // 关闭一端
            std::string processname = "process" + std::to_string(i);
            //把该进程的信息放进进程池中
            channels->push_back({pipefd[1], id, processname});
            oldfd.push_back(pipefd[1]);
            
            // sleep(1);
    }
    
}

简洁菜单:

void Menu()
{
    std::cout << "####################################################################" << std::endl;
    std::cout << "####################################################################" << std::endl;
    std::cout << "#######1.检查单词是否有问题    2.检查是否要更新版本###################" << std::endl;
    std::cout << "#######3. 查看下载进度   4.查看用户是否需要登录#######################" << std::endl;
    std::cout << "##########0.退出####################################################" << std::endl;
    std::cout << "####################################################################" << std::endl;
} 

控制进程模块:

void ctrlSlaver(const std::vector<channel>& channels)
{
    int count=0;
    while(true)
    {
        int select=0;
        Menu();
        std::cout<<"Please Enter# "<<std::endl;
        std::cin>>select;

        if(select<=0 ||select>5)
            break;
        
        //选择任务
        // int cmdcode=rand()%tasks.size();
        int cmdcode=select-1;
        //选择进程
        int processpos=rand()%tasks.size();
        std::cout<<"father say:"<<"cmdcode:"<<cmdcode<<" already send to "<<channels[processpos]._slaverid<<
            " process name: "<<channels[processpos]._processname<<std::endl;
        //发送任务
        write(channels[processpos]._comdfd,&cmdcode,sizeof(cmdcode));
        count++;
        if(count>=5)
            break;
        sleep(1);
    }

}

关闭进程

void QuitSlaver(const std::vector<channel>& channels)
{
    for(const auto e:channels)
    {
        close(e._comdfd);
        waitpid(e._slaverid,nullptr,0);
    }
}

注意:关闭进程这里不能写成这样:否则程序关闭了一个写端后,仍会卡着不退出,继续等待子进程退出。

  for(const auto &c : channels) close(c._cmdfd);
 
  for(const auto &c : channels) waitpid(c._slaverid, nullptr, 0);

为什么呢?

因为我们的子进程文件描述符表都来源于父进程,父进程创建子进程的时候,这个子进程的文件描述符表是存储前面创建的管道文件写端,你现在关闭了父进程的写端,但是在其他的进程仍存在着管道文件写端。

我们讲到过管道的几种工作方式:

现在就是第一种情况。因此会有一点点的bug。

那么,怎么解决它?

第一种方法:

按照上面代码的方法。

记录起来就的文件描述符表,用完就及时关闭!

创建子进程前,关闭比子进程创建时间还早的子进程w端。

因为父子进程会发生写时拷贝,即对拷贝数据进行各自修改,所以父子进程相互执行,不会把自己的w端关闭,及不会对同一个文件描述符关闭两次!

第二种方法:

   
     int last = channels.size()-1;
     for(int i = last; i >= 0; i--)
     {
         close(channels[i]._cmdfd);
         waitpid(channels[i]._slaverid, nullptr, 0);
     }

回收进程池资源的时候,从最后一个被创建的子进程开始回收起来!

因为子进程中只存在比自己创建还要早写端,到最后一个被创建的子进程的写端只有父进程存在。

ok,本次就到此结束了,希望大家一起进步!

最后,到了 本次鸡汤环节:

坚定信念,勇往直前。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值