进程池,记得看注释

简介: 进程池,记得看注释

我们要模拟的是什么呢?

模拟父进程控制子进程执行任务

我们该怎么去控制呢,我们控制这些子进程本质是去管理这些子进程,那么就离不开

先描述再组织,也就是我们把每个子进程的内核数据信息交给父进程管理,父进程调用

操作系统的接口去管理子进程。描述的话就是把每个子进程的信息描述起来,管理就是

父进程用操作系统提供的接口去控制子进程执行


我们先把框架搭起来

1.1创建管道

1.2创建子进程

子进程(1.3.0关闭自身写端,1.3.1 输入重定向,1.3.2子进程开始等待获取父进程发送的命令)

父进程(1.3.3关闭父进程的读端,1.4存放子进程信息)

父进程(发命令控制子进程)

父进程(回收子进程)

总结就是三步

1.创建子进程并收集子进程信息

2.控制子进程

3.回收子进程

下面我们跟着代码去探索本质

#include <iostream>
#include <cassert>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <cstdlib>
#include <string>
#include <vector>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <cerrno>
#include "Task.hpp"
#include <stdio.h>
Task T;
const int gnum = 5;
class EndPoint
{
private:
  static int num;
public:
  pid_t _child_id;
  int _write_fd;
  std::string processname;
public:
  EndPoint(int id,int fd):_child_id(id),_write_fd(fd)
  {
    char namebuffer[64];
    snprintf(namebuffer,sizeof(namebuffer),"porcess-%d[%d-%d]",num++,_child_id,_write_fd);
    processname = namebuffer;//运算符重载=
  } 
  ~EndPoint()
  {}
};
int EndPoint::num = 0;
//子进程接受到命令,执行任务
void WaitCommand()
{ 
  while(1)
  {
    int command;
    int n = read(0,&command,sizeof(int));//以字节流读取,我们要的是command,整形,读取也是
  //读 整形的4个字节
    std::cout<<"recv cmd:"<<command<<std::endl;
    if(n == 0)
    {
      std::cerr<<errno<<":"<<strerror(errno)<<std::endl;
      break;
    }
    else if(n == sizeof(int))
    {
      T.Execute(command);
    }
    else
    {
      std::cerr<<errno<<":"<<strerror(errno)<<std::endl;
      break;
    }
  }
}
void createProcesses(std::vector<EndPoint>* end_point)
{
  std::vector<int> fds;
  for(int i = 0;i < gnum;i++)
  { 
    //1.1创建管道
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    assert(n == 0);
    (void)n;
    //1.2创建进程
    pid_t id = fork();
    assert(id != -1);
    if(id == 0)
    {
      for(auto &fd:fds) close(fd);
      //这个点很重要,子进程关闭从父进程继承下来的写端
      //关闭自身写端
      close(pipefd[1]);
      //我们期望子进程在读取指令的时候 ,从标准输入读取
      //1.3.1 输入重定向
      //dup2
      dup2(pipefd[0],0);
      //1.3.2子进程开始等待获取父进程发送的命令
      WaitCommand();
      std::cout<<"子进程等待命令终于结束了,阻塞的好累"<<std::endl;
      //子进程退出,关闭读端
      close(pipefd[0]);
      exit(0); 
    }
    //父进程写入
    close(pipefd[0]);//1.3关闭父进程的读端
    //1.4 
    end_point->push_back(EndPoint(id,pipefd[1]));
    fds.push_back(pipefd[1]);
  }
}
void show_board()
{
   std::cout<<"#################"<<"0:执行日志任务"<<"  ##################"<<std::endl;
   std::cout<<"#################"<<"1:执行数据库任务"<<"##################"<<std::endl;
   std::cout<<"#################"<<"2:执行网络请求任务"<<"################"<<std::endl;
   std::cout<<"#################"<<"3:执行通行任务"<<" ##################"<<std::endl;
   std::cout<<"#################"<<">3:退出"<<" ##################"<<std::endl;
}
//负载均衡算法
void CtrlProcess(const std::vector<EndPoint>& end_point)
{
  //父进程进行写入命令
  int cnt = 0;//我们变成轮询式的
  show_board();
  while(1)
  {
     //1.选择任务
     int command = 0;
     std::cin>>command;
     if(command > 3) break;
     //2.选择进程
     cnt %= end_point.size();
     //3.下发任务
     std::cout<<end_point[cnt].processname<<std::endl; 
     write(end_point[cnt]._write_fd,&command,sizeof(command));
     cnt++;
     sleep(1);//我们写入之后,子进程读到命令,然后执行相应命令的任务,由于我们的执行速度很快,我们的父进程在执行向显示打印的时候
     //子进程也在打印,此时我们父子进程并发式的向子进程打印,那么就会去抢占数据,可能会数据改错,我们让父进程等一等,也就是让父进程执行的
     //操作系统知道不需要
  }
}
void WaitChild(const std::vector<EndPoint>& end_point)
{
   //子进程写端关闭,父进程读端关闭,要想子进程退出,因为子进程的读端
   //是受父进程的写端影响的,所以父进程写端关闭,读端读完自然就会退出该进程
   //还有要回收
    for(const auto &end: end_point) close(end._write_fd);
    //回收所有子进程
    int cnt = 0;
    for(const auto &end: end_point) 
    { 
      pid_t ret = waitpid(end._child_id,nullptr,0);
      if(ret > 0) 
      {
        std::cout<<"等待成功,id是:"<<ret<<std::endl;
        cnt++;
      }
    }
  //这段代码其实也有不足,只是碰巧把最后一个给关闭,因为倒着关闭刚好可以把所有写端关闭
  //所以不会堵塞,但是这种做法不合适——取巧,所以采用第二种做法,每次在创建
  //新的子进程的时候,
  // int cnt = 0;
  // for(const auto &end: end_point) 
  // {
  //     close(end._write_fd);
  //     pid_t ret = waitpid(end._child_id,nullptr,0);
  //     if(ret > 0) 
  //     {
  //       std::cout<<"等待成功,id是:"<<ret<<std::endl;
  //       cnt++;
  //     }
  // }
   if(cnt == end_point.size()) std::cout<<"父进程回收了所有子进程"<<std::endl;
   else std::cout<<"内存泄漏"<<std::endl;
}
int main()
{
  //1.先进行创建控制结构,父进程写,子进程读
  std::vector<EndPoint> end_point;
  createProcesses(&end_point);//子进程阻塞在那等待读命令,等待读命令的时候,这个函数的代码被父进程执行把进程id写入到EndPoint对象中
  //然后此时该子进程是堵塞在那等待命令,父进程进行执行循环体,以下同理
  //同理循环完后,其实是有五个子进程在那从管道中读,因为管道里没有数据,所以堵塞在那,等父进程向管道写入命令
  //然后父进程执行下面的父进程的写入命令的代码,我们每写一个命令,就是向对应管道写入数据,然后对应管道的子进程读入,执行任务
  //打个比方,就是说,我们此时父进程所控制的管道内是空的,是空的,所以对于管道的子进程就是堵塞(在读命令,因为管道内没数据)
  //此时我们父进程发送一个任务码给管道,让对应子进程读到了,那么该子进程执行该任务,此时还是堵塞的,(为什么呢,是因为我们的写端还没关闭,我们的读端就默认一直在等待命名,一直堵塞在那)
  //所以执行不了std::cout<<"子进程等待命令终于结束了,阻塞的好累"<<std::endl;
  //2.我们写成自动化的,也可以搞成交互式的
  CtrlProcess(end_point);
  WaitChild(end_point);
  return 0;
}

现象1

其实我们创建完所以子进程的时候,我们的子进程一直处在堵塞状态(等待接受命令)

只有当我们回收之后所有子进程后,所有子进程就会立马执行这个命令

回收后

现象2

理解所有子进程会继承父进程写端的时候再来看下面!!!

怎么回收?

关闭所有写端!!!

方法1:倒着回收

方法2:

代码中实现的就是方法2,看注释

那为什么读端是一样的?

"写端不同,读端相同"是一个和匿名管道有关的表述,主要是指当使用匿名管道时,父进程和多个子进程之间的通信方式。
在这种情况下,父进程会往管道中写入数据,而多个子进程则可以从同一个管道中读取数据进行处理。因此,从子进程的角度来看,读取数据的管道文件描述符都是相同的。
但对于父进程来说,它需要向多个不同的管道中写入数据,以便控制多个不同的子进程并发执行任务。所以从父进程的角度来看,写入数据的管道文件描述符是不同的。
因此,"写端不同,读端相同"的表述中,是从两种进程的角度来描述匿名管道的特性。在这种通信方式下,需要注意管道的缓存限制,防止写入数据超过缓存大小而导致阻塞,同时在完成任务后要关闭掉管道文件描述符。

相关文章
忘记LockSupport怎么用了?那我们举个有趣的小例子,永远记住它!
忘记LockSupport怎么用了?那我们举个有趣的小例子,永远记住它!
49 0
忘记LockSupport怎么用了?那我们举个有趣的小例子,永远记住它!
|
6月前
|
Linux 数据安全/隐私保护
Linux常用命令实例带注释
Linux常用命令实例带注释
65 0
|
6月前
本来能运行但, 过了一会报红: 包不存在
总结: 父子模块的版本还是最好保持一致.
85 1
|
6月前
|
C++
codeAction提供代码错误解决方案重要笔记
codeAction提供代码错误解决方案重要笔记
176 0
|
11月前
|
Cloud Native Go Windows
兄弟 Goland 咱能一次性将注释设置好不
兄弟 Goland 咱能一次性将注释设置好不
线程的创建等待及退出 代码源码举例
线程的创建等待及退出 代码源码举例
75 0
|
Unix Apache C++
给代码写注释时有哪些讲究?
给代码写注释时有哪些讲究?
157 0
给代码写注释时有哪些讲究?
多线程顺序运行的 4 种方法,面试随便问!
多线程顺序运行的 4 种方法,面试随便问!
244 0
|
Java
线程池的7种创建方式,强烈推荐你用它...(7)
线程池的7种创建方式,强烈推荐你用它...(7)
88 0
线程池的7种创建方式,强烈推荐你用它...(7)