简谈线程池

前言:

本文是针对进程池的简单理解和一些基础部分的优化,后续会推出线程池的简谈

代码如下

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:

cpp 复制代码
pipefd[0] 读端

pipefd[1] 写端

现在:

pipe

read write

pipefd0 pipefd1

2.fork()

cpp 复制代码
pid_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.管道资源泄漏

假设:

父:

cpp 复制代码
pipefd[1] 写
pipefd[0] 读

fork之后:

cpp 复制代码
父:
    read fd
    write fd


子:
    read fd
    write fd

注意:

fork复制的是文件描述符表。

所以父子都有两份。

但是实际设计:

父:

cpp 复制代码
只负责写

write fd 保留
read fd关闭

子:

cpp 复制代码
只负责读

read fd保留
write fd关闭

形成:

cpp 复制代码
Master

write
 |
 |
pipe
 |
 |
read

Worker

最关键的问题 : EOF

假设:

父准备结束:

cpp 复制代码
close(write_fd);

正常情况下:

子:

bash 复制代码
read(fd)

返回: 0 代表:所有写端都关闭了,没有数据了

于是:

cpp 复制代码
break;

退出。


但是如果子没有关闭自己的write:

cpp 复制代码
父:

write关闭


子:

write仍然存在

那么内核认为:

所以:

子:

cpp 复制代码
read()

继续阻塞

结果:

cpp 复制代码
父:
waitpid()

等待子退出


子:
read()

永远等

形成:

父子进程互相等待,程序卡死。

问题2:fork之后channels是不是同一个?

fork之后:父子拥有:两个完全独立的vector副本

问题3:为什么read循环里面不需要sleep?

这里是整个进程池设计的核心。

cpp 复制代码
while(true)
{
    read(fd,&task_code,sizeof(task_code));

    tasks[task_code]();
}

关键:

read不是一直占CPU

如果没有数据

cpp 复制代码
read()

会进入:

阻塞状态

例如:

cpp 复制代码
while

 |
read()

没有任务

 |
阻塞

CPU状态:0%

它不会while循环疯狂跑


如果加上

cpp 复制代码
sleep(1);

变成:

cpp 复制代码
while(true)
{
    sleep(1);

    read();
}

会发生:

延迟任务响应

例如:

父:

cpp 复制代码
10:00:00

发送任务

正常:

Worker:

cpp 复制代码
10:00:00.001

read收到

执行

加sleep:

cpp 复制代码
10: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)

  1. 任务队列(Task Queue)

结构变成:

cpp 复制代码
             Master

                |
                |
          任务队列

  +---+---+---+---+
  |T1 |T2 |T3 |T4 |
  +---+---+---+---+

                |

        Worker竞争任务

        /    |    \

      W1    W2    W3

重点变化:

以前:

cpp 复制代码
Master指定Worker

现在:

cpp 复制代码
Worker主动获取任务

也就是:

以前:

领导安排员工干活

现在:

员工去任务池领取工作

  1. Worker状态管理

增加:

cpp 复制代码
enum Status
{
    FREE,
    BUSY
};

每个Worker:

保存:

cpp 复制代码
pid

status

任务分配:

cpp 复制代码
找FREE Worker

发送任务

修改BUSY

任务完成:

cpp 复制代码
Master
  |
  |任务
  ↓
Worker


Worker
  |
  |完成通知
  ↓
Master

Worker通知:

cpp 复制代码
DONE

修改:

cpp 复制代码
FREE
  1. 完成通知管道

现在你的管道:

只有:

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对象。

所以:

不会触发:

  • 拷贝构造
  • 移动构造
  • 析构
  1. 生命周期更加明确

比如:

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;


};
相关推荐
李少兄1 小时前
JavaScript 运算符完全指南
java·开发语言·javascript
广州灵眸科技有限公司1 小时前
瑞芯微(EASY EAI)RV1126B display
开发语言·数据库·人工智能·科技·嵌入式硬件
Huangxy__1 小时前
java+ai 全栈项目
java·开发语言
Nil2081 小时前
leetcode 79单词搜索
开发语言·c#
智购科技智能售货柜1 小时前
2026自动售货机商品掉落声学计数方案:从麦克风到频谱识别的工程实践~YH
人工智能·算法
Blockchina1 小时前
AI Agent 写权限安全吗?Codex、Claude Code 与 GitHub Copilot 最小权限实战指南
开发语言·php
ctlover1 小时前
网络编程和多线程
开发语言·网络·python
神仙别闹1 小时前
基于C#实现(WinForm)文件管理系统
java·开发语言·c#
Mr_liu_6661 小时前
ns3-gym例子解析_基础例子与wifi例子_DQN(3)
开发语言·c++·python·dqn·ns3