Linux:简易进程池编写

设计概念

进程池,即我们可以预先创建一堆子进程和对应的管道。等父进程有任务时派发给子进程工作。这样就可以节省开辟进程的花销:

当没有任务时,即管道为空。那么子进程就是阻塞状态不会影响其他进程工作效率。

但是注意我们要将任务均衡地派发给子进程,即实现负载均衡

Channel

首先我们要设计一个方案让父进程能统一管理与子进程之间的管道,最好的方法就是封装一个类。

那么类的成员变量必然要有管道的写端fd、子进程的pid还可以有给子进程的编号。

cpp 复制代码
class Channel
{
public:
    Channel(int wfd,pid_t id,const std::string&name)
        :_wfd(wfd),_subprocessid(id),_name(name)
        {}
    int GetWfd(){return _wfd;}
    pid_t GetPidProcessId(){return _subprocessid;}
    std::string GetName(){return _name;}
    //关闭写端
    void ColseChannel()
    {
        close(_wfd);
    }
    //等待子进程
    void Wait()
    {
        pid_t rid=waitpid(_subprocessid,nullptr,0);
        if(rid>0)
        {
            std::cout<<"wait"<<rid<<"success!"<<std::endl;
        }
    }
    ~Channel(){}
private:
    int _wfd;
    pid_t _subprocessid;
    std::string _name;
};

初始化

接下来我们要初始化Channel数组。

根据要创建的worker个数num,我们可以写一个简单的for循环创建管道和子进程。由子进程关闭写端,父进程关闭读端,即可。

但是我们要注意一个细节,这里以创建两个子进程举例,这里先创建第一个worker:

然后分别关闭读写端:

此时我们创建第二个worker:

然后分别关闭读写端:

这时我们发现第二个子进程对第一个管道的写端并没有关闭,此时有可能造成父进程最后无法结束进程。

因此我们在创建新的worker时,要将前面的worker的写端关闭:

cpp 复制代码
void CreatChannelAndSub(int num,std::vector<Channel>*channels,task_t task)
{
    for(int i=0;i<num;i++)
    {
        int pipefd[2]={0};
        int n=pipe(pipefd);
        //创建管道失败
        if(n<0)
            exit(1);
        //创建子进程
        pid_t id=fork();
        if(id==0)
        {
            //非空就要关闭前面的写端
            if(!channels->empty())
            {
                for(auto&channel:*channels)channel.ColseChannel();
            }
            close(pipefd[1]);
            //重定向到标准输入
            dup2(pipefd[0],0);
            //回调函数
            task();
            close(pipefd[0]);
            exit(0);
        }
        //父进程
        std::string channel_name="Channel-"+std::to_string(i);
        close(pipefd[0]);
        channels->emplace_back(pipefd[1],id,channel_name);
    }
}

创建任务

我们来实现不同的任务以分配给子进程,首先重命名下函数指针用以实现回调函数:

随意实现三个简单的任务:

cpp 复制代码
void Print()
{
    std::cout<<"I am print task"<<std::endl;
}
void DownLoad()
{
    std::cout<<"I am download task"<<std::endl;
}
void Flush()
{
    std::cout<<"I am flush task"<<std::endl;
}

创建回调表:

分配任务:

cpp 复制代码
int SelectTask()
{
    return rand()%TaskNum;
}

执行任务:

cpp 复制代码
void ExcuteTask(int number)
{
    if(number<0||number>2)
        return;
    tasks[number];
}

子进程工作

以及有了上面的一系列任务,我们是时候给子进程工作了:

cpp 复制代码
void work()
{
    while(true)
    {
        int command=0;
        int n=read(0,&command,sizeof(command));
        if(n==sizeof(int))
        {
            std::cout<<"pid is:"<<getpid()<<"handler task"<<std::endl;
            ExcuteTask(command);
        }
        //写端关闭
        if(n==0)
        {
            std::cout<<"sub process:"<<getpid()<<" quit"<<std::endl;
            break;
        }
    }
}

轮询方案

前面提到我们要实现负载均衡,这里可以简单实现为轮询。即轮流给子进程派送任务:

cpp 复制代码
int NextChannel(int channelnum)
{
    static int next=0;
    int channel=next;
    next++;
    next%=channelnum;
    return next;
}

分配工作

子进程的工作也有了,轮询方案也有了,就可以给子进程正是分配工作了:

cpp 复制代码
void SendTaskCommand(Channel &channel,int taskcommand)
{
    write(channel.GetWfd(),&taskcommand,sizeof(int));
}

void ctrlProcessOnce(std::vector<Channel>&channels)
{
    sleep(1);
    //挑选任务
    int taskcommand=SelectTask();
    //挑选信道和进程
    int channel_index=NextChannel(channels.size());
    //发送任务
    SendTaskCommand(channels[channel_index],taskcommand);
    std::cout<<std::endl;
    std::cout<<"taskcommand:"<<taskcommand<<"channel:"<<channels[channel_index].GetName()\
    <<"sub process:"<<channels[channel_index].GetPidProcessId()<<std::endl;
}

void ctrlProcess(std::vector<Channel>&channels,int times=-1)
{
    if(times>0)
    {
        while(times--)
            ctrlProcessOnce(channels);
    }
    else
    {
         while(true)
            ctrlProcessOnce(channels);
    }
}

关闭子进程和管道

有了前面关闭子进程所有写端的处理,我们能直接关闭子进程的读端进而使子进程退出,对子进程等待即可:

cpp 复制代码
void CleanUpChannel(std::vector<Channel>&channels)
{
    for(auto&channel:channels)
    {
        channel.ColseChannel();
        channel.Wait();
    }
}

Main

接下来就是在main函数里安排代码执行的顺序,我们还可以通过选项的形式控制生成的子进程数量:

cpp 复制代码
int main(int argc,char *argv[])
{
    if(argc!=2)
    {
        std::cerr<<"Usage:"<<argv[0]<<" processnum"<<std::endl;
    }
    int num=std::stoi(argv[1]);
    std::vector<Channel>channels;
    //1.创建子进程和信道
    CreatChannelAndSub(num,&channels,work);
    //2.控制子进程
    ctrlProcess(channels,5);
    //3.回收资源
    CleanUpChannel(channels);
    
    return 0;
}

来尝试运行代码:

完整代码

相关推荐
AlfredZhao1 天前
生产环境里,为什么不建议把普通端口直接暴露到公网?
linux·https·443·80
戴为沐2 天前
Linux内存扩容指南
linux
zylyehuo3 天前
Linux 彻底且安全地删除文件
linux
用户805533698033 天前
主线 U-Boot 上 RK3506:和闭源 rkbin 拔河的三个隐性契约
linux·嵌入式
用户034095297913 天前
linux fcitx 5 雾凇拼音 设置在中文输入法下仍然输入英文标点
linux
Web3探索者5 天前
可视化服务器管理和传统命令行区别是什么?新手教程:Linux 运维到底该用图形界面还是 SSH 命令行?
linux·ssh
zylyehuo5 天前
Linux系统中网线与USB网络共享冲突
linux
Sokach10156 天前
Linux Shell 脚本从零到能用:一个新手的一天学习总结
linux
AlfredZhao7 天前
Docker 容器时区不对,`timedatectl` 不存在怎么办?
linux·timezone
zzzzzz3108 天前
9K Star 炸裂开源!这个 C 语言写的代码知识图谱,把 Linux 内核索引压缩到了 3 分钟
linux·服务器·sql