构建与优化自定义进程池

1. 什么是进程池?

简单来说,进程池就是预先创建固定数量的工作进程,通过设计任务队列或调度算法来分配任务给空闲的进程 —— 实现“负载均衡”。

2. 进程池框架设计

枚举错误返回值:

enum
{UsageError = 1,ArgError,PipeError
};
i. 设定子进程数量与格式验证

设置进程池的默认调用方式: ./processpool sub_process_nums

#include <iostream>
using namespace std;void Usage(char* argv)
{cout << "Usage:" << endl;cout << "\t" << "argv sub_process_nums" << endl;
}int main(int argc, char* argv[])
{if (argc < 2) // 命令行指令的本质是字符串, 字符串个数 < 2 时返回{Usage(argv[0]);return UsageError;}// ...return 0;
}
ii. 控制与管理子进程 之 " 创建通信管道与进程 "

在这部分内容开始之前,不妨先明晰一个问题: **“先创建管道再创建子进程,还是先创建子进程再创建管道?” **

要回答这个问题,我们需要了解 通信管道 的本质 及 fork 函数

  • 通信管道 (通信信道)

当你调用 pipe() 时,操作系统会在 文件描述符表 中为管道分配两个条目,一个用于写入 —— “写端”,一个用于读取 —— “读端”;

管道的 读端写端 本质上都是 文件描述符

  • fork 函数

fork() 的主要功能是,从当前进程(父进程)中创建一个新的进程(子进程);

子进程继承了父进程的资源限制、环境变量、打开的文件描述符表、工作目录等,但它们是独立的实体。

根据以上信息,不难得出:先创建管道、再创建子进程,子进程会继承父进程打开的文件描述符表,接着只需要关闭父进程的读端、子进程的写端,即可实现父子进程间的通信(反之亦然)。

#include <unistd.h>int main(int argc, char* argv[])
{int sub_process_nums = stoi(argv[1]); // c标准库中的函数,将字符串转整型if (sub_process_nums <= 0)	return ArgError;// 1. 创建通信管道与进程for (int i = 0; i < sub_process_nums; i++){int pipefd[2];int n = pipe(pipefd);// 创建管道成功返回 0,失败返回 -1if (n < 0)return PipeError;pid_t id = fork();if (id == 0){// child 负责读close(pipefd[1]); // 关闭写端// todoexit(0); // 执行完退出}// father 负责写close(pipefd[1]); // 关闭读端}// ...
}

int pipefd[2];

int n = pipe(pipefd); ——> 管道创建成功后,pipefd[0] 为 读端文件描述符,pipefd[1] 为 写端文件描述符。

3. 封装通信管道与进程池
i. class Channel

为了保存循环创建的通信管道和子进程信息,我们封装一个 通信管道 类型。

#include <string>class Channel
{
public:Channel(int wfd, pid_t process_id, const string& name):_wfd(wfd), _sub_process_id(process_id), _name(name){}// 观察父进程创建子进程时的现象void Debug(){cout << "_wfd: " << _wfd;cout << ", _sub_process_id: " << _sub_process_id;cout << ", _name: " << _name << endl;}// 增加获取管道各种信息的接口int Wfd() { return _wfd; }pid_t Pid() { return _sub_processs_id; }string Name() { return _name; }~Channel() {}
private:int _wfd; // 写端文件描述符pid_t _sub_process_id;string _name;
};

我们并未在 Channel 中封装读端文件描述符,因为我们将在每次循环中对 stdin 做重定向 —— dup2(pipefd[0], 0) ,之后子进程在运行时,只需要向 标准输入stdin —— 0 中读取任务指令即可。

通信管道本质上是文件,管道的读端和写端本质上是文件描述符;

dup2() 的工作原理,是将第一个参数指定的文件描述符,复制到第二个参数指定的位置。

ii. class ProcessPool

封装进程池,是为了更好地控制与管理子进程。

#include <vector>class ProcessPool
{
public:ProcessPool(int sub_process_num) :_sub_process_num(sub_process_num){} ~ProcessPool() {}int CreatChannels(work_t work) // 回调函数{// 1. 创建通信信道和进程for (int i = 0; i < _sub_process_num; i++){// 先创建管道int pipefd[2];int n = pipe(pipefd);if (n < 0){return PipeError;}// 再创建子进程,确保父进程和子进程读写同一根管道pid_t id = fork();if (id == 0){// child -> rclose(pipefd[1]);// TODOdup2(pipefd[0], 0); // 将 pipefd[0] 重定向work(pipefd[0]); // 方便后续在子进程中观察每个管道读端的文件描述符// sleep(100);exit(0);}// fatherclose(pipefd[0]);string cname = "channel--" + to_string(i);_channels.push_back(Channel(pipefd[1], id, cname));}return 0;}private:vector<Channel> _channels;int _sub_process_num;
};

int CreatChannels(work_t work) { } 中有一两个细节:

一为前文提到过的,重定向;

第二,即这个函数的参数 —— 这种编程模式也叫做 回调函数 —— 将函数作为参数传递给另一个函数,以便特定条件发生时供后者调用。

我们将子进程待执行的函数,作为参数传入 CreatChannels() 中供子进程调用,后续我们只需对传入参数(传入不同的函数)进行修改就可以让子进程执行不同的任务而不用对 CreadChannels() 函数体进行修改。

4. 负载均衡式任务调度
#include <stdlib.h>
#include <time.h>void CtrlProcess(ProcessPool* ptr, int cnt)
{while (cnt){// a. 选择一个通道和进程int channel = ptr->NextChannel();// b. 选择一个任务int task = NextTask();// c. 发送任务ptr->SendTask(channel, task);sleep(1); // 每隔 1s 发送一次任务--cnt;}
}int main()
{// ...// 1. 创建通信管道与进程ProcessPool *processpool_ptr = new ProcessPool(sub_process_num); // sub_process_num 为要创建子进程的个数processpool_ptr->CreatChannels(worker); // worker() 待补充srand(time(nullptr));// 2. 任务调度CtrlProcess(processpool_ptr, 10); // 假定 10 个任务cout << "task run done" << endl;// 3. 回收进程delete processpool_ptr;return 0;
}
  • 选择一个通道和进程
class ProcessPool
{
public:int NextChannel(){static int next = 0;int c = next;next++;next %= _channels.size(); // 防止越界return c;}
};
  • 选择一个任务
typedef void(*task_t)(int, pid_t); // 函数指针类型// 模拟任务
void PrintLog(int fd, pid_t id)
{cout << "channel rfd: " << fd << ", sub process: " << id << ", task: Print log task" << endl << endl;
}void ConnectMysql(int fd, pid_t id)
{cout << "channel rfd: " << fd << ", sub process: " << id << ", task: Connect mysql task" << endl << endl;
}void ReloadConf(int fd, pid_t id)
{cout << "channel rfd: " << fd << ", sub process: " << id << ", task: Reload conf task" << endl << endl;
}task_t tasks[3] = {PrintLog, ConnectMysql, ReloadConf};int NextTask()
{return rand() % 3; 
}
  • 发送任务
class ProcessPool
{
public:void SendTask(int index, int command){cout << "Send task to " << _channels[index].Name() << ", pid: " << _channels[index].Pid() << endl;write(_channels[index].Wfd(), &command, sizeof(command));}
};
5. 子进程任务执行:通过 worker() 读取父进程指令
typedef void(*work_t)(int);// 函数指针类型void worker(int fd)
{while (1){int code = 0;ssize_t n = read(0, &code, sizeof(code));if (n == sizeof(code)) // read 成功,返回值为读取到内容的大小/字节个数{if (code >= 3) continue;tasks[code](fd, getpid());}else if (n == 0) // 父进程关闭写端后,继续读,read 返回 0{cout << "sub process id: " <<  getpid() << " is to quit ..." << endl;break;}sleep(1);}
}
6. 回收子进程

设计 KillAll() ,完成子进程和管道的回收 —— 遍历进程池中的 _channels ,关闭管道的写端,读端将管道中的数据读完后,会读到返回值 0,表示读结束。

#include <sys/wait.h>class ProcessPool
{
public:void KillAll(){for (auto& channel : _channels){pid_t pid = channel.Pid(); // 子进程(管道读端进程)的 pidclose(channel.Wfd());pid_t rid = waitpid(pid, nullptr, 0);if (rid == pid) // wait 成功{cout << "wait sub process: " << pid << "success..." << endl;}cout << "close channel: " << channel.Name() << ", sub process is to quit.." << endl;}}
};
int main()
{// ... // 3. 回收进程processpool_ptr->KillAll();delete processpool_ptr;return 0;
}

程序运行情况如下:

在这里插入图片描述

从图中可以观察到两点信息:1. 每个读端文件描述符都是 3; 2. task run done 后,子进程并没有退出。

原因是,

1. 当父进程关闭管道的读端后, 原先分配给读端的文件描述符(3 号文件描述符)就会被释放;再次调用 pipe() 创建新管道时,OS 会重新分配这个最小未使用的文件描述符(3 号文件描述符)给新创建的管道

2.子进程通过 fork() 创建时,它会继承父进程所有打开的文件描述符。回收进程调用 KillAll() 时,尽管关闭了父进程的写端,子进程仍持有对原管道写端的引用,使得读端无法按预期读到返回值 0 ,进而无法关闭子进程。

要解决 子进程持有对原管道写端的引用 的问题,我们需要定义一个 vector<int> —— 用于保存父进程对所有管道的写端,接着让子进程在执行分配任务之前关闭所有写端 —— 修改 CreatProcess 函数。

int CreatChannels(work_t work){vector<int> fds;for (int i = 0; i < _sub_process_num; i++){int pipefd[2];int n = pipe(pipefd);if (n < 0){return PipeError;}fds.push_back(pipefd[1]); // 保存管道的写端pid_t id = fork();if (id == 0){// child -> r// close(pipefd[1]); // 不再需要单独关闭对应管道的写端if (!fds.empty()){for (auto& fd : fds){close(fd);}}// TODOdup2(pipefd[0], 0);work(pipefd[0]);// sleep(100);exit(0);}// fatherclose(pipefd[0]);string cname = "channel--" + to_string(i);_channels.push_back(Channel(pipefd[1], id, cname));}return 0;}

在这里插入图片描述

子进程正常退出,程序正常结束…

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

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

相关文章

人工智能时代的学与教

疫情逐渐散去之后&#xff0c;最最吸引全球目光的应该就是ChatGPT-3了。美国政治家亨利基辛格领衔出版的新书《AI世代与我们人类的未来》(The Age of AI and Our Human Future)中举了一个例子来说明ChatGPT-3的能力。首先让ChatGPT-3阅读关于它自身能力的哲学评论之后&#xff…

【吊打面试官系列-MySQL面试题】MyISAM 表格将在哪里存储,并且还提供其存储格式?

大家好&#xff0c;我是锋哥。今天分享关于【MyISAM 表格将在哪里存储&#xff0c;并且还提供其存储格式&#xff1f;】面试题&#xff0c;希望对大家有帮助&#xff1b; MyISAM 表格将在哪里存储&#xff0c;并且还提供其存储格式&#xff1f; 每个 MyISAM 表格以三种格式存储…

240912-设置WSL中的Ollama可在局域网访问

A. 最终效果 B. 设置Ollama&#xff08;前提&#xff09; sudo vim /etc/systemd/system/ollama.service[Unit] DescriptionOllama Service Afternetwork-online.target[Service] ExecStart/usr/bin/ollama serve Userollama Groupollama Restartalways RestartSec3 Environme…

读取t x t文件生成exce

读取t x t文件生成excel package com.moka.api.custom.core.controller; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermode…

LangChain基础知识大全

LangChain基础知识大全 一、部署ChatGLM-6B1.拉取源码2.安装环境3.下载模型4.修改api.py配置5.运行api.py 二、Models组件1.LLM&#xff08;大语言模型&#xff09;2.Chat Model&#xff08;聊天模型&#xff09;3.Embedding Model&#xff08;嵌入模型&#xff09;3.1 下载中文…

基于Python的自然语言处理系列(16):TorchText + CNN + Teacher Forcing

在本篇文章中&#xff0c;我们将实现 卷积序列到序列学习模型&#xff08;Convolutional Sequence to Sequence Learning&#xff09;。与之前介绍的基于循环神经网络&#xff08;RNN&#xff09;的模型不同&#xff0c;卷积模型不依赖递归成分&#xff0c;而是通过卷积层&…

增强现实系列—Map-Relative Pose Regression for Visual Re-Localization

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

基于JAVA+SpringBoot+Vue的社区智慧养老监护管理平台

基于JAVASpringBootVue的社区智慧养老监护管理平台 前言 ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN[新星计划]导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末附源码下载链接&#x1…

科研绘图系列:R语言多个AUC曲线图(multiple AUC curves)

文章目录 介绍加载R包导入数据数据预处理画图输出结果组图系统信息介绍 多个ROC曲线在同一张图上可以直观地展示和比较不同模型或方法的性能。这种图通常被称为ROC曲线图,它通过比较不同模型的ROC曲线下的面积(AUC)大小来比较模型的优劣。AUC值越大,模型的诊断或预测效果越…

前后端跨域问题及其在ThinkPHP中的解决方案

在现代Web开发中&#xff0c;前后端分离的架构越来越普遍&#xff0c;但这也带来了跨域问题。跨域指的是在一个域下的网页试图请求另一个域的资源&#xff0c;浏览器出于安全考虑会限制这种行为。本文将探讨如何在ThinkPHP中解决跨域问题。 #### 1. 什么是跨域&#xff1f; 跨…

一个皮肤科医生长痘的的自救

内服 复方锌铁钙口服液 丹参瞳胶囊 盐酸米诺环素胶囊 (每天一次) 内服 外用: 克林霉素甲硝搽剂 (泛红的痘痘) 人表皮生长因子(痘印)氢醌软膏 (点阵激光留下的色沉 早晚一次) 至少用两个月【痤疮|痘痘用药 一个皮肤科医生的自救】https://www.bilibili.com/video/BV1zu41…

算法题之每日温度

每日温度 给定一个整数数组 temperatures &#xff0c;表示每天的温度&#xff0c;返回一个数组 answer &#xff0c;其中 answer[i] 是指对于第 i 天&#xff0c;下一个更高温度出现在几天后。如果气温在这之后都不会升高&#xff0c;请在该位置用 0 来代替。 示例 1: 输入…

java计算机毕设课设—企业车辆管理系统(附源码、文章、相关截图、部署视频)

这是什么系统&#xff1f; 资源获取方式在最下方 java计算机毕设课设—企业车辆管理系统(附源码、文章、相关截图、部署视频) 企业车辆管理系统通过计算机&#xff0c;能够直接“透视”车辆使用情况&#xff0c;数据计算自动完成&#xff0c;尽量减少人工干预&#xff0c;可…

Java项目实战II基于Java+Spring Boot+MySQL的植物健康系统(开发文档+源码+数据库)

目录 目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 随着…

实战指南:深度剖析Servlet+JSP+JDBC技术栈下的用户CRUD操作

本博客总结基于MVC(JSPServletJDBC)操作用户信息的CRUD&#xff08;增删改查功能&#xff09;的完整小项目。包括图片上传和回显&#xff0c;模糊查询&#xff0c;过滤器的登录校验和设置全局字符集以及监听器统计在线用户人数等额外功能&#xff0c;因为代码较多&#xff0c;我…

UnLua实现继承

一、在蓝图中实现继承 1、创建父类&#xff0c;并绑定Lua脚本 2、创建子类蓝图&#xff0c;如果先创建的子类&#xff0c;可以修改父类继承 注意&#xff0c;提示选择继承父类的接口&#xff01; 二、在Lua中实现继承 1、在父类Lua脚本中实现函数 BP_CharacterBase.lua func…

构建数字化生态系统:打造数字化转型中开放协作平台的最佳实践和关键实施技巧

在数字化转型浪潮中&#xff0c;企业如何确保成功实施至关重要。除了技术上的革新&#xff0c;企业还必须在战略执行、架构优化以及合规性管理等方面掌握最佳实践。随着云计算、大数据、人工智能等新兴技术的迅速发展&#xff0c;企业通过正确的实施技巧不仅能提升业务效率&…

Qemu开发ARM篇-3、qemu运行uboot演示

文章目录 1、运行uboot2、qemu常用命令 在上一篇Qemu开发ARM篇-2、uboot交叉编译文章中&#xff0c;我们搭建了交叉编译工具链&#xff0c;并成功进行了uboot的交叉编译&#xff0c;在该篇中&#xff0c;我们将演示如何利用qemu运行上一篇中交叉编译的uboot程序。 1、运行uboo…

计算机毕业设计之:基于微信小程序的学生考勤系统的设计与实现(源码+文档+讲解)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

Redis——redispluspls库hash及zset类型相关接口使用

文章目录 hash类型相关接口hset和hgethexistshdelhkeys 和 hvalshmset和hmget zset类型相关接口zadd和zrangezcard 和 zremzscore和zrank hash类型相关接口 hset和hget std::cout<<"hset 和 hget"<<std::endl;redis.flushall();redis.hset("key&qu…