Linux--构建进程池

目录

1.进程池

1.1.我们先完成第一步,创建子进程和信道

[1.2. 通过channel控制,发送任务](#1.2. 通过channel控制,发送任务)

1.3回收管道和子进程

1.4进行测试

1.5完整代码


1.进程池

进程池其产生原因主要是为了优化大量任务需要多进程完成时频繁创建和删除进程所带来的资源消耗,以及更好的实现多个进程间的协同。以下是关于进程池的一些关键点:

  1. 组成 :进程池技术主要由两部分组成:资源进程和管理进程。资源进程是预先创建好的空闲进程,等待管理进程分发任务(maste向哪个管道写入,就是唤醒哪一个子进程处理任务)。管理进程则负责创建这些资源进程,将工作分配给空闲的资源进程处理,并在工作完成后回收这些资源进程。(父进程要进行后端任务的负载均衡)
  2. 管理:管理进程如何有效地管理资源进程是关键。这涉及到分配任务给资源进程、回收空闲资源进程等操作。管理进程和资源进程之间需要进行交互,这种交互可以通过管道(需要子进程执行一个任务我们就可以派发一个管道,连接一个子进程,执行任务)
  3. 使用场景 :当我们需要并行的处理大规模任务时,比如服务器处理大量客户端的任务,进程池是一个很好的选择。它可以避免频繁创建和销毁进程的开销,提高应用的响应速度。
  4. 优化:使用进程池可以避免动态创建和销毁进程的开销,提高应用的性能和稳定性。此外,进程池还可以根据任务的执行情况尽量减少创建的进程数量,最多创建指定个数的进程。

代码实现:

实现进程池首先我们需要提前准备好一批数量的进程,当一个任务队列被提交到线程池,每个工作进程都会不断从任务队列中取出任务并执行。


1.1.我们先完成第一步,创建子进程和信道

Channel类是来管理一个子进程和与之相关联的管道写端,这个Channel类就是信道。用于一个需要管理多个子进程和它们各自管道写端的程序中。例如,你可能有一个父进程,它创建多个子进程,每个子进程都通过管道与父进程通信。在这种情况下,你可以为每个子进程和它的管道写端 创建一个Channel对象,并使用这些对象来管理它们。

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 GetProcessId() { return _subprocessid; }
    std::string GetName() { return _name; }
    void CloseChannel()
    {
        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;//写端fd
    pid_t _subprocessid;//子进程id'
    std::string _name;//一个信道的名称
};

下面函数的主要逻辑是创建多个管道和相应的子进程,每个子进程都将从它自己的管道中读取数据,而父进程则保留管道的写端以便后续向子进程发送数据。同时,这个函数也维护了一个 Channel 对象的向量(std::vector<Channel>),其中每个 Channel 对象代表一个与特定子进程和管道写端相关联的通道。

task_t是一个函数指针类型,是为了泛型,将来可以传任意不同的任务交给子进程做。

可以理解为回调函数 task 的使用降低了任务与子进程之间的耦合度。在传统的编程模型中,你可能会为每个子进程编写特定的代码块,这些代码块直接嵌入在创建子进程的函数中。这样做会导致代码的高耦合性,因为每个子进程的行为都紧密地绑定在创建它们的函数中。(关于task的应用见下文)

dup2(pipefd[0], 0)将管道的读端,重定向到标准输入。意思就是管道的读端变成了标准输入,无需修改代码以处理文件描述符,因为以后任意的子进程去读取任务,都是从标准输入去读取的。

在子进程中,它关闭了管道的写端,将读端重定向到标准输入,执行给定的回调函数 task,然后关闭读端并退出。

cpp 复制代码
void CreateChannelAndSub(int num, std::vector<Channel> *channels, task_t task)
{
    // BUG? --> fix bug
    for (int i = 0; i < num; i++)
    {
        // 1. 创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        if (n < 0)
            exit(1);

        // 2. 创建子进程
        pid_t id = fork();
        if (id == 0)
        {
            if (!channels->empty())
            {
                // 第二次之后,开始创建的管道
                for(auto &channel : *channels) channel.CloseChannel();
            }
            // child - read
            close(pipefd[1]);
            dup2(pipefd[0], 0); // 将管道的读端,重定向到标准输入
            task();
            close(pipefd[0]);
            exit(0);
        }

        // 3.构建一个channel名称
        std::string channel_name = "Channel-" + std::to_string(i);
        // 父进程
        close(pipefd[0]);
        // a. 子进程的pid b. 父进程关心的管道的w端
        channels->push_back(Channel(pipefd[1], id, channel_name));
    }
}

提供一个任务文件,接下来让子进程执行任务:

cpp 复制代码
#pragma once

#include <iostream>
#include <ctime>
#include <cstdlib>
#include <sys/types.h>
#include <unistd.h>

#define TaskNum 3

typedef void (*task_t)(); // task_t 函数指针类型

void Print()
{
    std::cout << "I am print task" << std::endl;
}
void DownLoad()
{
    std::cout << "I am a download task" << std::endl;
}
void Flush()
{
    std::cout << "I am a flush task" << std::endl;
}

task_t tasks[TaskNum];

void LoadTask()
{
    srand(time(nullptr) ^ getpid() ^ 17777);
    tasks[0] = Print;
    tasks[1] = DownLoad;
    tasks[2] = Flush;
}

void ExcuteTask(int number)
{
    if (number < 0 || number > 2)
        return;
    tasks[number]();
}

int SelectTask()
{
    return rand() % TaskNum;
}

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);
        }
        else if (n == 0)
        {
            std::cout << "sub process : " << getpid() << " quit" << std::endl;
            break;
        }
    }
}
  • 初始化阶段:LoadTask()函数被调用,设置tasks函数指针数组以包含所有可用的任务函数,通过索引调用三个具体的任务函数:Print(), DownLoad(), 和 Flush()。
  • 任务选择阶段:SelectTask()函数在需要时用于生成一个随机任务索引。
  • 任务执行阶段:在子进程中,work()函数不断监听标准输入,等待一个任务索引。一旦接收到索引,就调用ExcuteTask()来执行相应的任务。

子进程退出:当子进程从标准输入读取到0时,它知道应该退出循环并结束。


1.2. 通过channel控制,发送任务

cpp 复制代码
int NextChannel(int channelnum)
{
    static int next = 0;
    int channel = next;
    next++;
    next %= channelnum;
    return channel;
}
void SendTaskCommand(Channel &channel, int taskcommand)
{
    write(channel.GetWfd(), &taskcommand, sizeof(taskcommand));
}
void ctrlProcessOnce(std::vector<Channel> &channels)
{
    sleep(1);
    // a. 选择一个任务
    int taskcommand = SelectTask();
    // b. 选择一个信道和进程
    int channel_index = NextChannel(channels.size());
    // c. 发送任务
    SendTaskCommand(channels[channel_index], taskcommand);
    std::cout << std::endl;
    std::cout << "taskcommand: " << taskcommand << " channel: "
              << channels[channel_index].GetName() << " sub process: " << channels[channel_index].GetProcessId() << std::endl;
}
void ctrlProcess(std::vector<Channel> &channels, int times = -1)
{
    if (times > 0)
    {
        while (times--)
        {
            ctrlProcessOnce(channels);
        }
    }
    else
    {
        while (true)
        {
            ctrlProcessOnce(channels);
        }
    }
}
  1. NextChannel 函数是一个轮询选择器,用于在给定的通道数量(channelnum)中循环选择一个通道索引。它使用了一个静态变量 next 来跟踪上一次选择的索引,并在每次调用时递增这个索引。
  2. SendTaskCommand 函数用于向指定的 Channel 对象(代表一个与子进程的通信通道)发送一个任务命令(taskcommand 。它通过调用 Channel 对象的 GetWfd 方法获取管道的写端文件描述符,然后使用 write 系统调用来将任务命令写入管道。这样,子进程就可以从管道中读取这个命令并执行相应的任务。
  3. ctrlProcessOnce 函数模拟了控制进程的一次操作 。调用 NextChannel 函数来选择一个通道索引,并通过该索引从 channels 向量中获取一个 Channel 对象。然后,它调用 SendTaskCommand 函数向选定的通道发送任务命令。最后,它打印出相关的信息,包括任务命令、通道名称和子进程的进程ID,以便进行调试和监控。
  4. ctrlProcess 函数是控制进程的主循环。它接受一个 channels 向量和一个可选的 times 参数。如果 times 大于0,则控制进程会循环执行 ctrlProcessOnce 函数 times 次;如果 times 小于或等于0(默认为-1),则控制进程会无限循环地执行 ctrlProcessOnce 函数

1.3回收管道和子进程

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 GetProcessId() { return _subprocessid; }
    std::string GetName() { return _name; }
    void CloseChannel()
    {
        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;//写端fd
    pid_t _subprocessid;//子进程id'
    std::string _name;//一个信道的名称
};

void CleanUpChannel(std::vector<Channel> &channels)
{

    for (auto &channel : channels)
    {
        channel.CloseChannel();
    }
    for (auto &channel : channels)
    {
        channel.Wait();
    }
}

a. 关闭所有的写端 b. 回收子进程

注意:一定是先关闭全部读端,然后统一读端,而不能关一个,等待一个。

在执行子进程创建逻辑的时候,是存在bug的。

我们在创建第一个信道的时候是没有什么问题的:

接着来看,我们创建第二个信道会发生什么:fd[4]的指向被第二个信道的子进程继承了,那么第一个管道的写端就个fd指向了,这就是坑

如果继续创建更多的信道,那么这种情况会一直累积,只有最后一个文件只有一个写端。如果我们是关一个等待一个,那么第一个信道,只会关闭一个读端,但还有其它的读端,这就会发生进程阻塞,导致管道文件一直在等待读取,无法被释放;如果统一的关闭读端,那么当关闭到最后一个信道的时候,最后一个信道读端只有一个,管道文件会被释放,然后就会递归式的逆向关闭其它信道的所有读端,最后将所有的管道文件释放,父进程只需要一 一等待,就能回收所有子进程了。


1.4进行测试

子进程执行任务成功,父进程回收子进程成功。


1.5完整代码

Task.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <ctime>
#include <cstdlib>
#include <sys/types.h>
#include <unistd.h>

#define TaskNum 3

typedef void (*task_t)(); // task_t 函数指针类型

void Print()
{
    std::cout << "I am print task" << std::endl;
}
void DownLoad()
{
    std::cout << "I am a download task" << std::endl;
}
void Flush()
{
    std::cout << "I am a flush task" << std::endl;
}

task_t tasks[TaskNum];

void LoadTask()
{
    srand(time(nullptr) ^ getpid() ^ 17777);
    tasks[0] = Print;
    tasks[1] = DownLoad;
    tasks[2] = Flush;
}

void ExcuteTask(int number)
{
    if (number < 0 || number > 2)
        return;
    tasks[number]();
}

int SelectTask()
{
    return rand() % TaskNum;
}

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);
        }
        else if (n == 0)
        {
            std::cout << "sub process : " << getpid() << " quit" << std::endl;
            break;
        }
    }
}

test.cc

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

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 GetProcessId() { return _subprocessid; }
    std::string GetName() { return _name; }
    void CloseChannel()
    {
        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;
};

//  task_t task: 回调函数
void CreateChannelAndSub(int num, std::vector<Channel> *channels, task_t task)
{
    // BUG? --> fix bug
    for (int i = 0; i < num; i++)
    {
        // 1. 创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        if (n < 0)
            exit(1);

        // 2. 创建子进程
        pid_t id = fork();
        if (id == 0)
        {
            if (!channels->empty())
            {
                // 第二次之后,开始创建的管道
                for(auto &channel : *channels) channel.CloseChannel();
            }
            // child - read
            close(pipefd[1]);
            dup2(pipefd[0], 0); // 将管道的读端,重定向到标准输入
            task();
            close(pipefd[0]);
            exit(0);
        }

        // 3.构建一个channel名称
        std::string channel_name = "Channel-" + std::to_string(i);
        // 父进程
        close(pipefd[0]);
        // a. 子进程的pid b. 父进程关心的管道的w端
        channels->push_back(Channel(pipefd[1], id, channel_name));
    }
}

int NextChannel(int channelnum)
{
    static int next = 0;
    int channel = next;
    next++;
    next %= channelnum;
    return channel;
}

void SendTaskCommand(Channel &channel, int taskcommand)
{
    write(channel.GetWfd(), &taskcommand, sizeof(taskcommand));
}
void ctrlProcessOnce(std::vector<Channel> &channels)
{
    sleep(1);
    // a. 选择一个任务
    int taskcommand = SelectTask();
    // b. 选择一个信道和进程
    int channel_index = NextChannel(channels.size());
    // c. 发送任务
    SendTaskCommand(channels[channel_index], taskcommand);
    std::cout << std::endl;
    std::cout << "taskcommand: " << taskcommand << " channel: "
              << channels[channel_index].GetName() << " sub process: " << channels[channel_index].GetProcessId() << std::endl;
}
void ctrlProcess(std::vector<Channel> &channels, int times = -1)
{
    if (times > 0)
    {
        while (times--)
        {
            ctrlProcessOnce(channels);
        }
    }
    else
    {
        while (true)
        {
            ctrlProcessOnce(channels);
        }
    }
}

void CleanUpChannel(std::vector<Channel> &channels)
{

    for (auto &channel : channels)
    {
        channel.CloseChannel();
        
    }
    for (auto &channel : channels)
    {
        
        channel.Wait();
    }
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " processnum" << std::endl;
        return 1;
    }
    int num = std::stoi(argv[1]);
    LoadTask();

    std::vector<Channel> channels;
    // 1. 创建信道和子进程
    CreateChannelAndSub(num, &channels, work);

    // 2. 通过channel控制子进程
    ctrlProcess(channels, 10);

    // 3. 回收管道和子进程. a. 关闭所有的写端 b. 回收子进程
    CleanUpChannel(channels);

    // sleep(100);
    return 0;
}#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp"

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 GetProcessId() { return _subprocessid; }
    std::string GetName() { return _name; }
    void CloseChannel()
    {
        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;
};

//  task_t task: 回调函数
void CreateChannelAndSub(int num, std::vector<Channel> *channels, task_t task)
{
    // BUG? --> fix bug
    for (int i = 0; i < num; i++)
    {
        // 1. 创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        if (n < 0)
            exit(1);

        // 2. 创建子进程
        pid_t id = fork();
        if (id == 0)
        {
            if (!channels->empty())
            {
                // 第二次之后,开始创建的管道
                for(auto &channel : *channels) channel.CloseChannel();
            }
            // child - read
            close(pipefd[1]);
            dup2(pipefd[0], 0); // 将管道的读端,重定向到标准输入
            task();
            close(pipefd[0]);
            exit(0);
        }

        // 3.构建一个channel名称
        std::string channel_name = "Channel-" + std::to_string(i);
        // 父进程
        close(pipefd[0]);
        // a. 子进程的pid b. 父进程关心的管道的w端
        channels->push_back(Channel(pipefd[1], id, channel_name));
    }
}

int NextChannel(int channelnum)
{
    static int next = 0;
    int channel = next;
    next++;
    next %= channelnum;
    return channel;
}

void SendTaskCommand(Channel &channel, int taskcommand)
{
    write(channel.GetWfd(), &taskcommand, sizeof(taskcommand));
}
void ctrlProcessOnce(std::vector<Channel> &channels)
{
    sleep(1);
    // a. 选择一个任务
    int taskcommand = SelectTask();
    // b. 选择一个信道和进程
    int channel_index = NextChannel(channels.size());
    // c. 发送任务
    SendTaskCommand(channels[channel_index], taskcommand);
    std::cout << std::endl;
    std::cout << "taskcommand: " << taskcommand << " channel: "
              << channels[channel_index].GetName() << " sub process: " << channels[channel_index].GetProcessId() << std::endl;
}
void ctrlProcess(std::vector<Channel> &channels, int times = -1)
{
    if (times > 0)
    {
        while (times--)
        {
            ctrlProcessOnce(channels);
        }
    }
    else
    {
        while (true)
        {
            ctrlProcessOnce(channels);
        }
    }
}

void CleanUpChannel(std::vector<Channel> &channels)
{

    for (auto &channel : channels)
    {
        channel.CloseChannel();
        
    }
    for (auto &channel : channels)
    {
        
        channel.Wait();
    }
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " processnum" << std::endl;
        return 1;
    }
    int num = std::stoi(argv[1]);
    LoadTask();

    std::vector<Channel> channels;
    // 1. 创建信道和子进程
    CreateChannelAndSub(num, &channels, work);

    // 2. 通过channel控制子进程
    ctrlProcess(channels, 10);

    // 3. 回收管道和子进程. a. 关闭所有的写端 b. 回收子进程
    CleanUpChannel(channels);

    // sleep(100);
    return 0;
}
相关推荐
cherub.2 分钟前
深入解析信号量:定义与环形队列生产消费模型剖析
linux·c++
梅见十柒25 分钟前
wsl2中kali linux下的docker使用教程(教程总结)
linux·经验分享·docker·云原生
Koi慢热28 分钟前
路由基础(全)
linux·网络·网络协议·安全
传而习乎39 分钟前
Linux:CentOS 7 解压 7zip 压缩的文件
linux·运维·centos
soulteary40 分钟前
突破内存限制:Mac Mini M2 服务器化实践指南
运维·服务器·redis·macos·arm·pika
我们的五年1 小时前
【Linux课程学习】:进程程序替换,execl,execv,execlp,execvp,execve,execle,execvpe函数
linux·c++·学习
爱吃青椒不爱吃西红柿‍️1 小时前
华为ASP与CSP是什么?
服务器·前端·数据库
IT果果日记1 小时前
ubuntu 安装 conda
linux·ubuntu·conda
Python私教1 小时前
ubuntu搭建k8s环境详细教程
linux·ubuntu·kubernetes
羑悻的小杀马特1 小时前
环境变量简介
linux