前言:
本文是针对进程池的简单理解和一些基础部分的优化,后续会推出线程池的简谈
代码如下
cpp
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <string>
#include <vector>
#include <functional>
#include <unistd.h>
#include <sys/wait.h>
///////////////////////////////子进程要完成的任务/////////////////////////
void SyncDisk()
{
std::cout << getpid() << ": 刷新数据到磁盘任务" << std::endl;
sleep(1);
}
void Download()
{
std::cout << getpid() << ": 下载数据到系统中" << std::endl;
sleep(1);
}
void PrintLog()
{
std::cout << getpid() << ": 打印日志到本地" << std::endl;
sleep(1);
}
void UpdateStatus()
{
std::cout << getpid() << ": 更新一次用户的状态" << std::endl;
sleep(1);
}
typedef void (*task_t)(); // 函数指针
task_t tasks[4] = {SyncDisk, Download, PrintLog, UpdateStatus}; // 任务表 ---面向对象化
///////////////////////////////进程池相关////////////////////////////////
enum
{
OK = 0,
PIPE_ERROR,
FORK_ERROR
};
// 子进程的入口函数
void DoTask(int fd)
{
while (true)
{
int task_code = 0;
ssize_t n = read(fd, &task_code, sizeof(task_code)); // 问题1: 子进程需要sleep吗?不需要!
if (n == sizeof(task_code))
{
if (task_code >= 0 && task_code < 4)
{
tasks[task_code](); // 执行任务表中的任务
}
}
else if (n == 0)
{
// 父进程要结束,我也应该要退出了!
std::cout << getpid() << ": task quit ..." << std::endl;
break;
}
else
{
perror("read");
break;
}
}
}
const int gprocessnum = 5;
using cb_t = std::function<void(int)>;
class ProcessPool
{
private:
// 父进程管理"通道"
class Channel
{
public:
Channel(int wfd, pid_t pid) : _wfd(wfd), _sub_pid(pid)
{
_sub_name = "sub-channel-" + std::to_string(_sub_pid);
}
~Channel()
{
}
void Write(int index)
{
ssize_t n = write(_wfd, &index, sizeof(index)); // 约定的4字节发送吗?
(void)n;
}
std::string Name()
{
return _sub_name;
}
void ClosePipe()
{
std::cout << "关闭wfd: " << _wfd << std::endl;
close(_wfd);
}
void Wait()
{
pid_t rid = waitpid(_sub_pid, nullptr, 0);
(void)rid;
std::cout << "回收子进程: " << _sub_pid << std::endl;
}
void PrintInfo()
{
printf("wfd: %d, who: %d, channel name: %s\n", _wfd, _sub_pid, _sub_name.c_str());
}
private:
int _wfd; // 1. wfd
pid_t _sub_pid; // 2. 子进程是谁
std::string _sub_name; // 3. 子channel的名字
// int cnt;
};
public:
ProcessPool()
{
srand((unsigned int)time(nullptr) ^ getpid());
}
~ProcessPool() {}
void Init(cb_t cb)
{
CreateProcessChannel(cb);
}
void Run()
{
int cnt = 10;
// while (cnt--)
while (true)
{
// std::cout << "------------------------------------------------" << std::endl;
// // 1. 选择一个任务
// int itask = SelectTask();
// std::cout << "itask: " << itask << std::endl;
// // 2. 选择一个channel(管道+子进程),本质是选择一个下标数字
// int index = SelectChannel();
// std::cout << "index: " << index << std::endl;
// // 3. 发送一个任务给指定的channel(管道+子进程)
// printf("发送 %d to %s\n", itask, channels[index].Name().c_str());
// SendTask2Salver(itask, index);
sleep(1); // 一秒一个任务
}
}
void Quit()
{
// version3:
for (auto &channel : channels)
{
channel.ClosePipe();
channel.Wait();
}
// version2: 逆向回收
// int end = channels.size()-1;
// while(end >= 0)
// {
// channels[end].ClosePipe();
// channels[end].Wait();
// end--;
// }
// bug演示
// for (auto &channel : channels) // 为什么不能这样写?应该怎么写?bug?
// {
// channel.ClosePipe();
// channel.Wait();
// }
// version1
// // 1. 让所有子进程退出
// for (auto &channel : channels)
// {
// channel.ClosePipe();
// }
// // 2. 回收子进程
// for (auto &channel : channels)
// {
// channel.Wait();
// }
}
void Debug()
{
for (auto &c : channels)
{
c.PrintInfo();
}
}
private:
void SendTask2Salver(int itask, int index)
{
if (itask >= 4 || itask < 0)
return;
if (index < 0 || index >= channels.size())
return;
channels[index].Write(itask);
}
int SelectChannel()
{
static int index = 0;
int selected = index;
index++;
index %= channels.size();
return selected;
}
int SelectTask()
{
int itask = rand() % 4;
return itask;
}
void CreateProcessChannel(cb_t cb)
{
// 1. 创建多个管道和创建多个进程
for (int i = 0; i < gprocessnum; i++)
{
int pipefd[2] = {0};
int n = pipe(pipefd);
if (n < 0)
{
std::cerr << "pipe create error" << std::endl;
exit(PIPE_ERROR);
}
pid_t id = fork();
if (id < 0)
{
std::cerr << "fork error" << std::endl;
exit(FORK_ERROR);
}
else if (id == 0)
{
// 子进程关闭历史wfd, 影响的是自己的fd表
if(!channels.empty())
{
for(auto &channel : channels)
channel.ClosePipe();
}
// child
close(pipefd[1]); // read
cb(pipefd[0]); // 回调: 让子进程调用出去,回调完成,他还会回来的!!!
exit(OK); // 根本不会执行后续代码,执行完自己的DoTask()函数之后,自己就退出了
}
else
{
// 父进程 write
close(pipefd[0]);
channels.emplace_back(pipefd[1], id);
// Channel ch(pipefd[1], id);
// channels.push_back(ch);
std::cout << "创建子进程: " << id << " 成功..." << std::endl;
sleep(1);
}
}
}
private:
// 0. 未来组织所有channel的容器
std::vector<Channel> channels;
};
int main()
{
// 1. 初始化进程池
ProcessPool pp;
pp.Init(DoTask);
pp.Debug();
// 2. 父进程控制子进程,channels
pp.Run();
// 3. 释放和回收所有资源(释放管道,回收子进程)
pp.Quit();
return 0;
}
1. 代码review
一.代码整体目标
这段代码实现的是一个简单的:父进程管理多个子进程的进程池模型
结构:
父进程 Master
|
| | | |
pipe1 pipe2 pipe3 pipe4 ...
| | | |
Worker1 Worker2 Worker3 Worker4
父进程负责:
1.创建子进程
2.创建通信通道
3.保存每个子进程的信息
4.给子进程派发任务
5.关闭通道
6.回收子进程
子进程负责:
1.等待父进程发送任务
2.读取任务编号
3.根据编号执行函数
例如: 父进程:write(pipefd, 0) 发送0
子进程:write(pipefd, 0) 收到0 执行SyncDisk()
二.程序入口main()
cpp
int main()
{
ProcessPool pp;
pp.Init(DoTask);
pp.Debug();
pp.Run();
pp.Quit();
}
执行顺序:
cpp
main
|
|
创建ProcessPool对象
|
|
Init()
|
|
创建5个子进程
|
|
Debug打印channel信息
|
|
Run()
|
|
Quit()
|
|
退出
但此时:没有子进程
因为 ProcessPool pp; 仅仅是创建了一个对象
cpp
vector<Channel> channels;
现在 channels.size()==0
三.进入Init()
cpp
pp.Init(DoTask);
进入:
cpp
void Init(cb_t cb)
{
CreateProcessChannel(cb);
}
这里的cb是什么???
cb是:
cpp
using cb_t = std::function<void(int)>;
所以cb可以代表:
一个参数为int
返回void
的函数
传入:DoTask
也就是说:
父进程创建子进程后:
子进程执行:
cpp
DoTask(pipefd[0])
四.创建第一个子进程
进入:
cpp
CreateProcessChannel()
循环:
cpp
for(int i=0;i<gprocessnum;i++)
其中:
cpp
gprocessnum=5
所以创建五个worker
第一次循环
1.创建管道
cpp
int pipefd[2];
pipe(pipefd);
Linux pipe:
返回两个fd:
cpppipefd[0] 读端 pipefd[1] 写端现在:
pipe
read write
pipefd0 pipefd1
2.fork()
cpppid_t id=fork();fork之后:父子拥有:完全相同的代码和数据副本
包括: channels
但是 不是共享
而是
父:
channels(A)子:
channels(A的复制品)
五.fork之后分支
cpp
if(id==0)
表示子进程。
子进程执行
cpp
else if(id==0)
{
}
首先:
关闭历史wfd:
cpp
if(!channels.empty())
{
for(auto& channel:channels)
channel.ClosePipe();
}
为什么?
因为:
后面循环会不断fork。
比如:
第5个子进程产生时:
它复制了父进程之前保存的:
cpp
channel1
channel2
channel3
channel4
里面包含:
cpp
write fd
但是子进程根本不用写。
所以关闭。
否则:
会产生严重问题:
管道EOF问题(重点)
父:
关闭写端:
cpp
close(write_fd)
子:
read():
应该返回:
cpp
0
代表:父没任务了。
但是如果:还有其他进程持有write_fd
那么:Linux认为:还有人在写。
所以:read永远阻塞。 管道引用计数导致无法EOF
子关闭:
cpp
close(pipefd[1]);
因为子只读。
形成:
cpp
子:
关闭write
保留read
read(pipefd[0])
然后:
cpp
cb(pipefd[0]);
实际上:执行
cpp
DoTask(pipefd[0])
进入:
cpp
while(true)
六、Worker进入等待状态
子进程:
cpp
while(true)
{
read(fd,&task_code,sizeof(task_code))
}
现在:
它阻塞在:
cpp
read()
等待父进程发送任务
状态:
cpp
Worker1:
read阻塞
等待任务
七.父进程继续
fork返回:
子: 0
父: 子进程PID
进入:
cpp
else
执行:
cpp
close(pipefd[0]);
为什么->>>>>父不用读
所以:
cpp
父:
关闭read
保留write
然后:
cpp
channels.emplace_back(pipefd[1],id);
保存:
cpp
写端fd
子PID
现在:
父:
cpp
channels:
[
{
wfd=4,
pid=2000
}
]
八、继续循环创建剩余4个Worker
最终:
父:
cpp
channels
------------------------------------------------
index wfd pid
0 4 2001
1 6 2002
2 8 2003
3 10 2004
4 12 2005
------------------------------------------------
同时:
5个子进程:
cpp
Worker1
Worker2
Worker3
Worker4
Worker5
全部阻塞:
read(pipe)
九、Debug()
执行:
cpp
pp.Debug();
打印:
cpp
wfd:4 who:2001
wfd:6 who:2002
...
注意:
这个时候:
父拥有:五个写端
子拥有:五个读端
十、进入 Run()
cpp
while(true)
{
sleep(1);
}
现在:
程序卡死在这里。
为什么?
因为任务分发代码被注释了:
cpp
// SelectTask()
// SelectChannel()
// SendTask
所以:
目前:
cpp
父进程:
睡眠
睡眠
睡眠
子进程:
read等待
read等待
read等待
没有任务发生。
如果打开任务分发
cpp
int task=SelectTask();
int index=SelectChannel();
SendTask2Salver(task,index);
流程:
假设:
task=2
index=3
父:
cpp
write(pipe4,2)
Worker4:
read:
cpp
task_code=2
执行:
cpp
PrintLog();
输出:
cpp
2004:打印日志到本地
十一、退出阶段 Quit()
执行:
cpp
pp.Quit();
核心:
cpp
for(auto& channel:channels)
{
channel.ClosePipe();
channel.Wait();
}
第一步:
关闭写端:
cpp
close(wfd)
例如:
父关闭:
cpp
pipe1 write
此时:
Worker1:
read返回:0
进入:
cpp
else if(n==0)
{
break;
}
退出while。
然后:
子:
cpp
exit(0)
父:
waitpid:
cpp
waitpid(pid)
整体运行图:
cpp
main
|
|
创建ProcessPool
|
|
Init
|
|
for循环5次
|
|
pipe()
|
|
fork()
|
+------------+
| |
父 子
| |
close读 close写
保存channel DoTask()
| |
继续fork read等待
|
Debug
|
Run
|
write任务
|
Worker执行
|
Quit
|
关闭pipe
|
Worker退出
|
wait回收
|
结束
2.问题整理
问题1:为什么父子进程必须关闭不用的管道端?
1.管道资源泄漏
假设:
父:
cpppipefd[1] 写 pipefd[0] 读fork之后:
cpp父: read fd write fd 子: read fd write fd注意:
fork复制的是文件描述符表。
所以父子都有两份。
但是实际设计:
父:
cpp只负责写 write fd 保留 read fd关闭子:
cpp只负责读 read fd保留 write fd关闭形成:
cppMaster write | | pipe | | read Worker最关键的问题 : EOF
假设:
父准备结束:
cppclose(write_fd);正常情况下:
子:
bashread(fd)返回: 0 代表:所有写端都关闭了,没有数据了
于是:
cppbreak;退出。
但是如果子没有关闭自己的write:
cpp父: write关闭 子: write仍然存在那么内核认为:
所以:
子:
cppread()继续阻塞
结果:
cpp父: waitpid() 等待子退出 子: read() 永远等形成:
父子进程互相等待,程序卡死。
问题2:fork之后channels是不是同一个?
fork之后:父子拥有:两个完全独立的vector副本
问题3:为什么read循环里面不需要sleep?
这里是整个进程池设计的核心。
cppwhile(true) { read(fd,&task_code,sizeof(task_code)); tasks[task_code](); }关键:
read不是一直占CPU
如果没有数据
cppread()会进入:
阻塞状态
例如:
cppwhile | read() 没有任务 | 阻塞CPU状态:0%
它不会while循环疯狂跑
如果加上
cppsleep(1);变成:
cppwhile(true) { sleep(1); read(); }会发生:
延迟任务响应
例如:
父:
cpp10:00:00 发送任务正常:
Worker:
cpp10:00:00.001 read收到 执行加sleep:
cpp10:00:00 任务来了 Worker正在sleep 10:00:01 醒来 read 执行所以:
为什么服务器程序喜欢阻塞IO?
因为:
等待事件的时候:
不消耗CPU。
比如:
nginx:
cpp等待请求 ↓ 阻塞 ↓ 请求来了 ↓ 处理
我们的代码:
实际上是:
cpp
Master
产生任务
选择Worker
发送任务编号
Worker
执行任务
类似:
cpp
Apache prefork模型
但是存在一个问题
看这里:
cpp
int SelectChannel()
{
static int index=0;
int selected=index;
index++;
index%=channels.size();
return selected;
}
它选择Worker的方法是什么?
轮询
任务:
cpp
1
2
3
4
5
6
7
分配:
cpp
Worker1:
1,6
Worker2:
2,7
Worker3:
3
Worker4:
4
Worker5:
5
问题:
假设:
Worker1执行:
cpp
SyncDisk()
{
sleep(100);
}
Worker2:
cpp
UpdateStatus()
{
sleep(1);
}
现在:
任务:
cpp
task1 -> Worker1
task2 -> Worker2
task3 -> Worker3
...
Worker1卡住。
但是:
Master仍然:
cpp
task4
task5
task6
继续发给Worker
显然这段代码负载不均衡
在真正的工程生产及系统中不会简单轮询,而是引入:任务队列(Task Queue)
- 任务队列(Task Queue)
结构变成:
cpp
Master
|
|
任务队列
+---+---+---+---+
|T1 |T2 |T3 |T4 |
+---+---+---+---+
|
Worker竞争任务
/ | \
W1 W2 W3
重点变化:
以前:
cpp
Master指定Worker
现在:
cpp
Worker主动获取任务
也就是:
以前:
领导安排员工干活
现在:
员工去任务池领取工作
- Worker状态管理
增加:
cpp
enum Status
{
FREE,
BUSY
};
每个Worker:
保存:
cpp
pid
status
任务分配:
cpp
找FREE Worker
发送任务
修改BUSY
任务完成:
cpp
Master
|
|任务
↓
Worker
Worker
|
|完成通知
↓
Master
Worker通知:
cpp
DONE
修改:
cpp
FREE
- 完成通知管道
现在你的管道:
只有:
cpp
Master ---> Worker
单向。
生产模型:
需要:
cpp
Master
|
|任务
↓
Worker
Worker
|
|完成通知
↓
Master
也就是:
双向通信。
问题四:为什么 Quit() 中:可能导致死锁?
cpp
for(auto& channel:channels)
{
channel.ClosePipe();
channel.Wait();
}
假设:
5个Worker。
父这样退出:
cpp
关闭Worker1管道
等待Worker1退出
关闭Worker2管道
等待Worker2退出
...
可能卡在:
cpp
channel.Wait();
直觉可能是:
我关闭了Worker1的写端,它应该退出,然后Wait成功
但是这种情况对于一个Worker是成立的,但是对于多个Worker则不然
关键在于:fork复制了文件描述符
假设:
创建Worker1 :
父:
cpp
channels:
[
pipe1 write
]
Worker1继承:
cpp
pipe1 write
pipe1 read
然后关闭历史fd。
很好。
继续创建Worker2。
此时:
父:
cpp
channels:
[
pipe1 write
]
Worker2继承:
cpp
pipe1 write
pipe2 read
pipe2 write
虽然Worker2不使用pipe1。
但是如果没有关闭:
Worker2仍然持有:
cpp
pipe1 write
于是形成:
cpp
pipe1:
Worker1 read
父 write
Worker2 write
Worker3 write
Worker4 write
Worker5 write
现在退出。
父:
cpp
channels[0].ClosePipe();
关闭:
cpp
父的pipe1 write
你认为:
pipe1没有写端
实际上还有:
cpp
Worker2 write
Worker3 write
Worker4 write
Worker5 write
所以:
Worker1:
cpp
read(pipe1)
不会返回0。
它会继续等待。
于是:
父:
cpp
waitpid(Worker1)
等待数据。
双方:
cpp
父:
waitpid
|
|
等待Worker1
Worker1:
read
|
|
等待EOF
死锁
这就是为什么子进程创建时有这一段:
cpp
if(!channels.empty())
{
for(auto& channel:channels)
channel.ClosePipe();
}
它的作用就是:
清理fork复制来的无用写端。
保证:
每个pipe:
只有:
cpp
一个父write
一个子read
但是还有第二个问题
cpp
for()
{
ClosePipe();
Wait();
}
假设:
Worker1执行:
cpp
SyncDisk()
{
sleep(100);
}
此时:
父:
cpp
close Worker1 pipe
wait Worker1
Worker1:
正在任务里面:
cpp
sleep(100)
它还没有回到:
cpp
read()
所以:
虽然管道关闭了:
但是Worker1还没退出。
父:
cpp
waitpid()
只能等。
结果:
Worker2、Worker3、Worker4、Worker5:
都还没关闭。
所以退出流程更好的方式:
不是:
cpp
关闭一个
等待一个
而是:
第一步:
关闭所有写端:
cpp
for(auto& channel:channels)
{
channel.ClosePipe();
}
此时:
所有Worker:
收到EOF。
第二步:
统一回收:
cpp
for(auto& channel:channels)
{
channel.Wait();
}
问题五:为什么 channels.emplace_back(pipefd1, id);可能导致vector扩容后文件描述符管理出现问题?为什么很多工程代码不直接保存对象,而是保存指针
cpp
std::vector<Channel> channels;
里面保存的是:
cpp
Channel对象
不是指针。
你的代码:
cpp
channels.emplace_back(pipefd[1], id);
实际上:
创建:
cpp
Channel对象
{
_wfd = 4
_sub_pid = 2001
}
放进vector。
问题出在:
vector不是无限增长的
比如:
一开始:
cpp
capacity = 1
存放第一个:
cpp
vector内存:
+-----------+
| Channel1 |
+-----------+
现在第二个来了
cpp
channels.emplace_back(pipefd[2],id);
但是容量不够。 怎么办?
vector重新申请:
cpp
旧空间:
+-----------+
| Channel1 |
+-----------+
新空间:
+-----------+-----------+
| Channel1 | Channel2 |
+-----------+-----------+
然后:
把旧对象搬进去
Channel里面有什么?
其中:
cpp
int _wfd
它代表:
Linux内核中的文件描述符
vector搬迁对象会发生什么?
假设:
旧对象:
cpp
Channel1
_wfd = 4
移动:
cpp
新对象
_wfd = 4
表面:
没问题
但是:
就对象什么时候销毁? vector释放旧空间
调用:
cpp
Channel::~Channel()
cpp
~Channel()
{
close(_wfd);
}
如此会发生问题:
旧对象vector中保存有 _wfd = 4,移动到新对象vector _wfd = 4,然后旧对象析构,结果真正的fd已经关闭了,后面的操作就无法进行了。
这就是为什么工程里经常不用vector存对象
在工程项目中vector不直接保存对象,而是保存对象的指针
1.避免对象移动
例如:
cpp
vector<Channel*> channels;
里面保存:
cpp
地址
地址
地址
而不是:
Channel对象。
所以:
不会触发:
- 拷贝构造
- 移动构造
- 析构
- 生命周期更加明确
比如:
cpp
auto ch=new Channel(fd,pid);
channels.push_back(ch);
什么时候释放:
cpp
delete ch;
由你控制
但是指针也需要注意 裸指针 会导致内存泄漏
所以现在C++更喜欢
cpp
std::unique_ptr<Channel>
例如:
cpp
vector<unique_ptr<Channel>> channels;
结构:
cpp
vector
|
|
unique_ptr
|
|
Channel对象
vector移动: 只移动unique_ptr,对象不会动
RALL:资源生命周期绑定对象生命周期
例如:
cpp
class Pipe
{
public:
Pipe(int fd)
:_fd(fd)
{}
~Pipe()
{
close(_fd);
}
private:
int _fd;
};
那么:
cpp
{
Pipe p(fd);
}//离开作用域自动close
假设:
cpp
class Channel
{
public:
~Channel()
{
close(_wfd);
}
private:
int _wfd;
};
并且:
cpp
vector<Channel> channels;
然后执行:
cpp
channels.emplace_back(fd,pid);
现在vector发生扩容。
问题六:为什么移动构造函数会成为这里的关键?如果Channels没有自己定义移动构造,会发生什么?
如果没有移动构造,可能使用拷贝。
例如:
cpp
Channel(const Channel& c)
{
_wfd=c._wfd;
}
结果:
旧对象:
cpp
Channel A
fd=4
新对象:
cpp
Channel B
fd=4
注意:
两个对象都认为:自己拥有fd = 4,但是Linux fd不是C++对象。
真实情况:
cpp
A
|
fd 4
|
pipe
B
|
fd 4
|
pipe
两个对象指向同一个资源
然后vector释放旧空间:
cpp
调用:
A.~Channel()
执行
close(4);
结果
pipe关闭
但是B里面:
_wfd=4
还存在
所以
B认为
我要write fd 4
实际上
write(4,...)
得到
Bad file descriptor
正确方式:
移动构造
首先明确移动构造不是 复制
例如:
cpp
Channel(Channel&& c)
{
_wfd=c._wfd;
c._wfd=-1;
}
发生:
移动前:
cpp
旧对象A:
_wfd=4
新对象B:
不存在
移动:
cpp
B:
_wfd=4
A:
_wfd=-1
然后旧对象析构:
cpp
close(-1);
不会关闭A
最终:
cpp
B拥有fd4
pipe正常
所有权转移
标准资源类设计模式
像Linux fd、socket、mutex、文件句柄这种资源:
cpp
class Channel
{
public:
Channel(int fd)
:_fd(fd)
{}
// 禁止复制
Channel(const Channel&)=delete;
Channel& operator=(const Channel&)=delete;
// 支持移动
Channel(Channel&& other)
{
_fd=other._fd;
other._fd=-1;
}
~Channel()
{
if(_fd!=-1)
close(_fd);
}
private:
int _fd;
};
这就是典型的:
RALL资源管理类
3.现代ProcessPool
一、整体架构
cpp
Master进程
|
|
Task Queue
|
---------------------
| | |
| | |
Worker1 Worker2 Worker3
↑ ↑ ↑
| | |
双向Pipe通信
二.完整代码
头文件:
cpp
#include <iostream>
#include <vector>
#include <memory>
#include <queue>
#include <string>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <cstring>
#include <functional>
using namespace std;
三.任务定义
cpp
// ================================
// Task类型
// ================================
// 子进程执行任务编号
enum TaskType
{
DOWNLOAD = 0,
SYNC_DISK,
PRINT_LOG,
UPDATE_STATUS
};
// 任务结构体
struct Task
{
int id;
};
四.任务执行函数
cpp
void ExecuteTask(int id)
{
switch(id)
{
case DOWNLOAD:
cout
<< getpid()
<< " 下载数据"
<< endl;
sleep(1);
break;
case SYNC_DISK:
cout
<< getpid()
<< " 同步磁盘"
<< endl;
sleep(1);
break;
case PRINT_LOG:
cout
<< getpid()
<< " 打印日志"
<< endl;
sleep(1);
break;
case UPDATE_STATUS:
cout
<< getpid()
<< " 更新状态"
<< endl;
sleep(1);
break;
}
}
五、RAII封装文件描述符(重点)
这是现代C++和你的代码最大区别。
cpp
class FileDescriptor
{
public:
FileDescriptor(int fd=-1)
:_fd(fd)
{}
~FileDescriptor()
{
Close();
}
//禁止复制
FileDescriptor(const FileDescriptor&)
=delete;
FileDescriptor&
operator=(const FileDescriptor&)
=delete;
//支持移动
FileDescriptor(FileDescriptor&& other)
{
_fd=other._fd;
other._fd=-1;
}
void Close()
{
if(_fd!=-1)
{
close(_fd);
_fd=-1;
}
}
int Get()
{
return _fd;
}
private:
int _fd;
};
六、Worker对象
cpp
class Worker
{
public:
Worker(int fd,pid_t pid)
:
_channel(fd),
_pid(pid)
{}
pid_t Pid()
{
return _pid;
}
void SendTask(Task task)
{
write(
_channel.Get(),
&task,
sizeof(task)
);
}
void Wait()
{
waitpid(
_pid,
nullptr,
0
);
}
private:
// RAII管理fd
FileDescriptor _channel;
// 子进程pid
pid_t _pid;
};
七、ProcessPool核心
cpp
class ProcessPool
{
public:
ProcessPool(int num)
{
CreateWorkers(num);
}
~ProcessPool()
{
Stop();
}
void Dispatch(Task task)
{
//简单轮询
int index =
_cursor % _workers.size();
_cursor++;
_workers[index]
->SendTask(task);
}
void Stop()
{
cout
<<"关闭进程池"
<<endl;
for(auto& worker:_workers)
{
Task quit;
quit.id=-1;
worker->SendTask(quit);
}
for(auto& worker:_workers)
{
worker->Wait();
}
}
private:
void CreateWorkers(int num)
{
for(int i=0;i<num;i++)
{
int pipefd[2];
pipe(pipefd);
pid_t pid=fork();
if(pid==0)
{
// child
close(pipefd[1]);
while(true)
{
Task task;
read(
pipefd[0],
&task,
sizeof(task)
);
if(task.id==-1)
break;
ExecuteTask(task.id);
}
exit(0);
}
else
{
// parent
close(pipefd[0]);
/*
unique_ptr管理Worker生命周期
vector扩容:
移动的是unique_ptr
不移动Worker对象
*/
_workers.push_back(
make_unique<Worker>(
pipefd[1],
pid
)
);
}
}
}
private:
vector<unique_ptr<Worker>>
_workers;
size_t _cursor=0;
};