朋友们、伙计们,我们又见面了,本期来给大家带来进程池的相关代码和知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!
C 语 言 专 栏:C语言:从入门到精通
数据结构专栏:数据结构
个 人 主 页 :stackY、
C + + 专 栏 :C++
Linux 专 栏 :Linux
目录
1. 进程池
创建进程我们调用的是fork系统调用,系统调用也是有成本的,当我们有很多需要执行的任务时,与其再分配任务的时候一个一个创建,倒不如先创建一批进程,然后将任务进行分配,这样做可以大大提高效率,这种方式就叫做进程池。
2. 进程池的简单实现
我们使用匿名管道的方式由主进程向各个子进程分配任务。
所以通信方式是父进程写入,子进程读取。
2.1 创建管道和进程
每个子进程和父进程传输任务都需要有对应的一个“信道”,所以我们使用循环创建进程并创建管道,为了正常分配任务,所以需要关闭不需要的文件描述符,形成单向信道(父进程写入,子进程读取)。并且我们将管道进行重定向,直接从标准输入里面读取即可。
#include <iostream> #include <unistd.h> #include <cassert> #define NUM 5 // 创建进程以及管道的个数 int main() { for(int i = 0; i < NUM; i++) { // 创建管道 int pipefd[2]; int n = pipe(pipefd); assert(n == 0); (void)n; // 防止告警 // 创建进程 pid_t id = fork(); assert(id != -1); if(id == 0) { // child // 构建单向信道 close(pipefd[1]); dup2(pipefd[0], 0); // 将标准输入指定为从pipefd[0]中读取 // ...... exit(0); } // parent close(pipefd[0]); } return 0; }
2.1.1 管理信道
我们创建的多个管道、进程,当主进程通过管道给子进程分配任务的时候,如何知道通过哪个管道分配给了哪个进程,所以也需要对信道进行管理(先描述再组织),所以创建一个channel的类,类成员设置:写给哪个管道、哪个进程读取、信道名称。
// 定义信道 class channel { public: channel(int fd, pid_t id) : ctrlfd(fd), workid(id) { name = "channel-" + std::to_string(number++); } public: int ctrlfd; pid_t workid; std::string name; };
定义好信道,所以还需要将其管理起来,通过vector将这些信道管理,将信道的管理工作变成了对数组的增删查改,创建一个信道,我们就添加一个信道。
顺带我们将创建信道以及创建管道的过程封装起来:
// 创建信道、创建进程 void CreateChannels(std::vector<channel> *c) { for (int i = 0; i < NUM; i++) { // 创建管道 int pipefd[2]; int n = pipe(pipefd); assert(n == 0); (void)n; // 防止告警 // 创建进程 pid_t id = fork(); assert(id != -1); if (id == 0) { // child // 构建单向信道 close(pipefd[1]); // ...... exit(0); } // parent close(pipefd[0]); // 添加信道 c->push_back(channel(pipefd[1], id)); } }
2.2 完成任务
完成任务首先都有任务列表,所以我们就用简单的打印函数来演示一下,创建一个Task.hpp文件作为任务列表:
#pragma once #include <iostream> #include <unistd.h> #include <sys/types.h> #include <functional> #include <vector> typedef std::function<void()> task_t; void DownLoad() { std::cout << "正在进行下载" << std::endl << " 处理者: " << getpid() << std::endl; } void PrintLog() { std::cout << "正在进行打印日志" << std::endl << " 处理者: " << getpid() << std::endl; } void PushVideoStream() { std::cout << "正在进行推送视频流" << std::endl << " 处理者: " << getpid() << std::endl; } class Init { public: // 任务码 const static int g_down_load_code = 0; const static int g_print_log_code = 1; const static int g_push_videostream_code = 2; // 任务集合 std::vector<task_t> tasks; public: Init() { // 添加任务至任务列表 tasks.push_back(DownLoad); tasks.push_back(PrintLog); tasks.push_back(PushVideoStream); } // 检查任务的合法性 bool CheckSafe(int code) { if (code <= 0 && code < tasks.size()) return true; else return false; } }; // 定义对象 Init init;
2.2.1 选择任务
选择任务可以在任务列表中通过任务码随机选择任务;
直接在任务列表里面添加选择任务的成员函数:
class Init { public: // 任务码 const static int g_down_load_code = 0; const static int g_print_log_code = 1; const static int g_push_videostream_code = 2; // 任务集合 std::vector<task_t> tasks; public: Init() { // 添加任务至任务列表 tasks.push_back(DownLoad); tasks.push_back(PrintLog); tasks.push_back(PushVideoStream); // 设置随机数 srand(time(nullptr) ^ getpid()); } // 运行任务 void RunTask(int code) { return tasks[code](); } // 随机选择任务 int SelectTask() { return rand() % tasks.size(); } std::string ToDesc(int code) { switch (code) { case g_down_load_code: return "Download"; case g_print_log_code: return "PrintLog"; case g_push_videostream_code: return "PushVideoStream"; default: return "Unknow"; } } };
2.2.2 选择信道
选择信道我们采用轮询的方式选择:
int main() { std::vector<channel> channels; // 创建进程和管道 CreateChannels(&channels); // 开始完成任务 int pos = 0; while(true) { // 1. 选择任务 int command = init.SelectTask(); // 2. 选择信道 const auto& c = channels[pos++]; pos %= channels.size(); // 3. 发送任务 } return 0; }
2.2.3 发送任务
使用write系统调用,直接向标准输入里面写入,将控制描述符和任务编号发送给子进程。
int main() { std::vector<channel> channels; // 创建进程和管道 CreateChannels(&channels); // 开始完成任务 int pos = 0; while(true) { // 1. 选择任务 int command = init.SelectTask(); // 2. 选择信道 const auto& c = channels[pos++]; pos %= channels.size(); // 3. 发送任务 write(c.ctrlfd, &command, sizeof(command)); } return 0; }
2.2.4 接受任务
为了方便,我们直接将管道重定向至标准输入,在接受任务时,直接从标准输入里面接受任务,我们规定标准一次按照四字节读取,在接受到任务之后先得判断任务的合法性,再去执行任务。
void Work() { while (true) { // 读取任务 int code = 0; ssize_t n = read(0, &code, sizeof(code)); if (n == sizeof(code)) { // 读成功 if (!init.CheckSafe(code)) // 任务合法性判断 continue; init.RunTask(code); } else if (n == 0) { // 失败 break; } else { // Do Nothing } } }
2.2.5 封装
将选择任务、选择信道、发送任务封装在一起,并且想实现一个给定次数执行多少次任务,如果没有指定,就一直发送:
void SendCommand(const std::vector<channel> &channels, bool flag, int num = -1) { int pos = 0; while (true) { // 1. 选择任务 int command = init.SelectTask(); // 2. 选择信道 auto &channel = channels[pos++]; pos %= channels.size(); // debug std::cout << "send command " << init.ToDesc(command) << "[" << command << "]" << " in " << channel.name << " worker is : " << channel.workid << std::endl; // 3. 发送任务 write(channel.ctrlfd, &command, sizeof(command)); // 4. 判断是否要退出 if (!flag) { num--; if (num <= 0) break; } sleep(1); } std::cout << "SendCommand done..." << std::endl; }
2.3 回收资源
在子进程退出之后父进程首先需要对子进程进行等待,还需要回收信道,根据管道的读写情况,当管道的写端关闭,OS就会杀掉写端进程,所以回收资源我们先等待子进程,再关闭写端:
void ReleaseChannel(const std::vector<channel> &c) { for (auto &channels : c) { close(channels.ctrlfd); pid_t rid = waitpid(channels.workid, nullptr, 0); if (rid == channels.workid) { std::cout << "wait child: " << channels.workid << " success" << std::endl; } } }
2.4 处理创建进程和管道时细节问题
第一次父进程创建子进程的时候,子进程会继承父进程的文件描述符表,但是在第二次创建子进程的时候,该子进程也会将父进程的文件描述符表继承下去,此时的文件描述符表还有指向第一次的管道,继承了父进程的写端,这就导致了在回收资源的时候会出错。
在回收时可以采用先全部关闭所有的管道,再等待子进程;
还可以采用倒着回收资源的方式。
还可以修改创建进程的方式:
定义临时的容器, 关闭每个子进程不需要的写端。
void Printfd(const std::vector<int> &fds) { std::cout << getpid() << " close fds: "; for (auto fd : fds) { std::cout << fd << " "; } std::cout << std::endl; } void CreateChannels(std::vector<channel> *c) { std::vector<int> temp; for (int i = 0; i < num; i++) { // 1.定义并创建管道 int pipefd[2] = {0}; int n = pipe(pipefd); assert(n == 0); (void)n; // 2.创建进程 pid_t id = fork(); assert(id != -1); // 3.构建单向通信信道 if (id == 0) // child { if (!temp.empty()) { for (auto fd : temp) { close(fd); } Printfd(temp); // 打印子进程关闭不需要的写端 } close(pipefd[1]); dup2(pipefd[0], 0); // 将标准输入指定为从pipefd[0]中读取 Work(); exit(0); } // father close(pipefd[0]); c->push_back(channel(pipefd[1], id)); temp.push_back(pipefd[1]); // 将父进程打开的写端记录下来 } }
3. 完整代码
Task.hpp:
#pragma once #include <iostream> #include <functional> #include <vector> #include <ctime> #include <unistd.h> typedef std::function<void()> task_t; void DownLoad() { std::cout << "正在进行下载" << std::endl << " 处理者: " << getpid() << std::endl; } void PrintLog() { std::cout << "正在进行打印日志" << std::endl << " 处理者: " << getpid() << std::endl; } void PushVideoStream() { std::cout << "正在进行推送视频流" << std::endl << " 处理者: " << getpid() << std::endl; } class Init { public: // 任务码 const static int g_down_load_code = 0; const static int g_print_log_code = 1; const static int g_push_videostream_code = 2; // 任务集合 std::vector<task_t> tasks; public: Init() { tasks.push_back(DownLoad); tasks.push_back(PrintLog); tasks.push_back(PushVideoStream); srand(time(nullptr) ^ getpid()); } bool CheckSafe(int code) { if (code <= 0 && code < tasks.size()) return true; else return false; } void RunTask(int code) { return tasks[code](); } int SelectTask() { return rand() % tasks.size(); } std::string ToDesc(int code) { switch (code) { case g_down_load_code: return "Download"; case g_print_log_code: return "PrintLog"; case g_push_videostream_code: return "PushVideoStream"; default: return "Unknow"; } } }; // 定义对象 Init init;
源ProcessPool.cc:
#include <iostream> #include <unistd.h> #include <cassert> #include <vector> #include <string> #include <sys/types.h> #include <sys/wait.h> #include "Task.hpp" const static int num = 5; static int number = 1; // 信道的定义 class channel { public: channel(int fd, pid_t id) : ctrlfd(fd), workid(id) { name = "channel-" + std::to_string(number++); } public: int ctrlfd; pid_t workid; std::string name; }; void Work() { while (true) { int code = 0; ssize_t n = read(0, &code, sizeof(code)); if (n == sizeof(code)) { // 读成功 if (!init.CheckSafe(code)) continue; init.RunTask(code); } else if (n == 0) { // 失败 break; } else { // Do Nothing } } } void Printfd(const std::vector<int> &fds) { std::cout << getpid() << " close fds: "; for (auto fd : fds) { std::cout << fd << " "; } std::cout << std::endl; } // 创建管道和进程 void CreateChannels(std::vector<channel> *c) { std::vector<int> temp; for (int i = 0; i < num; i++) { // 1.定义并创建管道 int pipefd[2] = {0}; int n = pipe(pipefd); assert(n == 0); (void)n; // 2.创建进程 pid_t id = fork(); assert(id != -1); // 3.构建单向通信信道 if (id == 0) // child { if (!temp.empty()) { for (auto fd : temp) { close(fd); } Printfd(temp); // 打印子进程关闭不需要的写端 } close(pipefd[1]); dup2(pipefd[0], 0); // 将标准输入指定为从pipefd[0]中读取 Work(); exit(0); } // father close(pipefd[0]); c->push_back(channel(pipefd[1], id)); temp.push_back(pipefd[1]); // 将父进程打开的写端记录下来 } } // 发送任务 void SendCommand(const std::vector<channel> &channels, bool flag, int num = -1) { int pos = 0; while (true) { // 1. 选择任务 int command = init.SelectTask(); // 2. 选择信道 auto &channel = channels[pos++]; pos %= channels.size(); // debug std::cout << "send command " << init.ToDesc(command) << "[" << command << "]" << " in " << channel.name << " worker is : " << channel.workid << std::endl; // 3. 发送任务 write(channel.ctrlfd, &command, sizeof(command)); // 4. 判断是否要退出 if (!flag) { num--; if (num <= 0) break; } sleep(1); } std::cout << "SendCommand done..." << std::endl; } // 回收资源 void ReleaseChannel(const std::vector<channel> &c) { for (auto &channels : c) { close(channels.ctrlfd); pid_t rid = waitpid(channels.workid, nullptr, 0); if (rid == channels.workid) { std::cout << "wait child: " << channels.workid << " success" << std::endl; } } } int main() { std::vector<channel> channels; // 1. 创建信道,创建进程 CreateChannels(&channels); // 2. 开始发送任务 const bool g_always_loop = true; // SendCommand(channels, g_always_loop); // 一直执行 SendCommand(channels, !g_always_loop, 10); // 执行10次 // 3. 回收资源,想让子进程退出,并且释放管道,只要关闭写端 ReleaseChannel(channels); return 0; }
朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,欲知后事如何,请听下回分解~,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!