网络编程(5)------ Reactor实现(v4)
回顾
v3 我们把服务器封装成了 TcpServer,调用方三行代码就能起一个反应堆:
cpp
TcpServer server("0.0.0.0",8080);
server.setAllCallback(onNewConnection,onMessage,onClose);
server.start();
但它的毛病在 v3 结尾已经点出来了:串行执行。
事件循环是单线程的,onMessage 也在这一条线程里跑。只要有一个业务的处理时间比较长(查库、算数据、写日志文件、甚至只是一个 sleep(1)),整个反应堆就停摆------其它连接的读写全部排队。这不是"慢一点",是一台连接卡死,全网卡死。
所以 v4 要干的事只有一件:把"计算"从事件循环里剥出去,交给线程池去做。
先想清楚三个问题
问题一:谁是 IO 线程,谁是计算线程
改造后的分工:
| 线程 | 职责 |
|---|---|
IO 线程 (跑 EventLoop::loop 的那条) |
accept、recv、send、事件分发。绝不执行耗时业务 |
| 计算线程(线程池里的 N 条) | 执行 onMessage 里那些真正耗时的业务逻辑 |
IO 线程只把数据"读出来"就立刻返回,继续去处理下一个就绪的 fd;真正的加工丢给线程池。
【图 1:v3 串行处理 与 v4 计算/IO 分离 的对比示意图】(已生成:
output/reactor-v3-vs-v4.png,上传到 CSDN 即可)
问题二:业务线程算完了,怎么把结果发回去
这是最容易写错的地方。业务线程算完之后,不能直接调 con->send(),原因有两个:
send()内部是writen------一次send可能对应多次write。如果两个线程同时往同一个 fd 写,几条消息会交织在一起,客户端收到的就是乱码。- 业务线程执行的时候,这条连接可能已经在 IO 线程里被判定为"对端关闭"并从
_conns里删掉了。此时往一个已经关闭的 fd 上写,轻则EPIPE,重则踩到已经被复用的 fd。
正确的做法是:业务线程不自己发,而是把"发送"这件事包装成一个任务,投递回 IO 线程去执行。
这就是 EventLoop::runInLoop(Task_t && cb) 的语义------"把这个函数放到 loop 所属的那条线程里执行"。
问题三:怎么让正阻塞着的 epoll_wait 立刻醒来
runInLoop 只是把任务塞进一个队列,可 IO 线程这时正卡在 epoll_wait 上睡觉呢,谁来叫它?
- 方案 A :把
epoll_wait的超时时间调到很小(比如 1ms)。能行,但等于忙等,CPU 白烧。 - 方案 B :注册一个"通知用"的 fd 到 epoll 上,别的线程想唤醒它,就往这个 fd 里写点东西,
epoll_wait立刻返回可读事件。
方案 B 才是正常思路。而这个"通知用"的 fd,Linux 专门给我们准备了一个:eventfd。
c
int eventfd(unsigned int initval, int flags);
它本质是内核维护的一个 64 位计数器:
- 往里面
write一个uint64_t,计数器加上这个值,fd 变为可读; - 从里面
read,把当前计数值读出来并清零,fd 变回不可读。
比起"用管道 pipe() 自己跟自己通信"的老套路,eventfd 只占一个 fd(管道要两个),而且语义就是"通知",非常贴切。
v4 的整体结构
【图 2:Reactor_v4 类图】------ 参考 v3 的类图,新增三处:ThreadPool/TaskQueue 两个类;EventLoop 多出 _evtfd、_pendings、_mutex 三个成员和 runInLoop 一族函数;TcpConnection 多出一个指向 EventLoop 的指针。
#mermaid-svg-O941O3Q7DOCBZXPh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-O941O3Q7DOCBZXPh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-O941O3Q7DOCBZXPh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-O941O3Q7DOCBZXPh .error-icon{fill:#552222;}#mermaid-svg-O941O3Q7DOCBZXPh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-O941O3Q7DOCBZXPh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-O941O3Q7DOCBZXPh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-O941O3Q7DOCBZXPh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-O941O3Q7DOCBZXPh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-O941O3Q7DOCBZXPh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-O941O3Q7DOCBZXPh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-O941O3Q7DOCBZXPh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-O941O3Q7DOCBZXPh .marker.cross{stroke:#333333;}#mermaid-svg-O941O3Q7DOCBZXPh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-O941O3Q7DOCBZXPh p{margin:0;}#mermaid-svg-O941O3Q7DOCBZXPh g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-O941O3Q7DOCBZXPh g.classGroup text .title{font-weight:bolder;}#mermaid-svg-O941O3Q7DOCBZXPh .cluster-label text{fill:#333;}#mermaid-svg-O941O3Q7DOCBZXPh .cluster-label span{color:#333;}#mermaid-svg-O941O3Q7DOCBZXPh .cluster-label span p{background-color:transparent;}#mermaid-svg-O941O3Q7DOCBZXPh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-O941O3Q7DOCBZXPh .cluster text{fill:#333;}#mermaid-svg-O941O3Q7DOCBZXPh .cluster span{color:#333;}#mermaid-svg-O941O3Q7DOCBZXPh .nodeLabel,#mermaid-svg-O941O3Q7DOCBZXPh .edgeLabel{color:#131300;}#mermaid-svg-O941O3Q7DOCBZXPh .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-O941O3Q7DOCBZXPh .label text{fill:#131300;}#mermaid-svg-O941O3Q7DOCBZXPh .labelBkg{background:#ECECFF;}#mermaid-svg-O941O3Q7DOCBZXPh .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-O941O3Q7DOCBZXPh .classTitle{font-weight:bolder;}#mermaid-svg-O941O3Q7DOCBZXPh .node rect,#mermaid-svg-O941O3Q7DOCBZXPh .node circle,#mermaid-svg-O941O3Q7DOCBZXPh .node ellipse,#mermaid-svg-O941O3Q7DOCBZXPh .node polygon,#mermaid-svg-O941O3Q7DOCBZXPh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-O941O3Q7DOCBZXPh .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-O941O3Q7DOCBZXPh g.clickable{cursor:pointer;}#mermaid-svg-O941O3Q7DOCBZXPh g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-O941O3Q7DOCBZXPh g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-O941O3Q7DOCBZXPh .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-O941O3Q7DOCBZXPh .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-O941O3Q7DOCBZXPh .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-O941O3Q7DOCBZXPh .dashed-line{stroke-dasharray:3;}#mermaid-svg-O941O3Q7DOCBZXPh .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-O941O3Q7DOCBZXPh #compositionStart,#mermaid-svg-O941O3Q7DOCBZXPh .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-O941O3Q7DOCBZXPh #compositionEnd,#mermaid-svg-O941O3Q7DOCBZXPh .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-O941O3Q7DOCBZXPh #dependencyStart,#mermaid-svg-O941O3Q7DOCBZXPh .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-O941O3Q7DOCBZXPh #dependencyStart,#mermaid-svg-O941O3Q7DOCBZXPh .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-O941O3Q7DOCBZXPh #extensionStart,#mermaid-svg-O941O3Q7DOCBZXPh .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-O941O3Q7DOCBZXPh #extensionEnd,#mermaid-svg-O941O3Q7DOCBZXPh .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-O941O3Q7DOCBZXPh #aggregationStart,#mermaid-svg-O941O3Q7DOCBZXPh .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-O941O3Q7DOCBZXPh #aggregationEnd,#mermaid-svg-O941O3Q7DOCBZXPh .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-O941O3Q7DOCBZXPh #lollipopStart,#mermaid-svg-O941O3Q7DOCBZXPh .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-O941O3Q7DOCBZXPh #lollipopEnd,#mermaid-svg-O941O3Q7DOCBZXPh .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-O941O3Q7DOCBZXPh .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-O941O3Q7DOCBZXPh .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-O941O3Q7DOCBZXPh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-O941O3Q7DOCBZXPh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-O941O3Q7DOCBZXPh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 引用
管理(map)
runInLoop 回投
业务线程
TcpServer
-Acceptor _acceptor
-EventLoop _loop
+start()
+stop()
+setAllCallback(cb1, cb2, cb3)
EventLoop
-int _epfd
-int _evtfd
-vector<Task_t> _pendings
-mutex _mutex
-map<int,TcpConnectionPtr> _conns
+loop()
+runInLoop(cb)
+wakeup()
+handleRead()
+doPendingFunctors()
TcpConnection
-SocketIO _sockIO
-EventLoop* _loop
+send(msg)
+sendInLoop(msg)
ThreadPool
-vector<thread> _threads
-TaskQueue _taskQue
+start()
+stop()
+addTask(task)
TaskQueue
-queue<Task_t> _que
-mutex _mutex
-condition_variable _notEmpty
-condition_variable _notFull
+push(task)
+pop() : Task_t
Acceptor
新增组件
TaskQueue:线程安全的任务队列
线程池和反应堆都要用到"队列"这个东西,所以先把它单独抽出来。它跟我们之前写线程池那篇文章里的实现是一样的:一把互斥锁 + 两个条件变量 + 一个容量上限。
TaskQueue.h
cpp
#ifndef __TASK_QUEUE_H
#define __TASK_QUEUE_H
#include<queue>
#include<mutex>
#include<condition_variable>
#include<functional>
using std::queue;
using std::mutex;
using std::condition_variable;
using std::function;
using Task_t = function<void()>;
class TaskQueue{
public:
TaskQueue();
TaskQueue(size_t cap);
~TaskQueue();
void push(Task_t && ptask);
Task_t pop();
bool full();
bool empty();
void wakeup();
private:
size_t _capacity;
queue<Task_t> _que;
mutex _mutex;
condition_variable _notEmpty;
condition_variable _notFull;
bool _flag;
};
#endif
TaskQueue.cpp
cpp
/* ************************************************************************
> File Name: TaskQueue.cpp
> Author: hyj
> mail: 1683958261@qq.com
> Created Time: Mon 03 Aug 2026 05:07:16 PM CST
> Description:
************************************************************************/
#include"TaskQueue.h"
using std::unique_lock;
// 委托构造
TaskQueue::TaskQueue():TaskQueue(20){
}
// 构造
TaskQueue::TaskQueue(size_t cap)
:_capacity(cap)
,_que()
,_mutex()
,_notEmpty()
,_notFull()
,_flag(true){
}
// 析构
TaskQueue::~TaskQueue(){
}
// 加入一个任务
void TaskQueue::push(Task_t && ptask){
unique_lock<mutex> mtx(_mutex);
while(full()){
_notFull.wait(mtx);
}
_que.push(std::move(ptask));
_notEmpty.notify_one();
}
// 弹出一个任务
Task_t TaskQueue::pop(){
unique_lock<mutex> mtx(_mutex);
while(empty() && _flag){
_notEmpty.wait(mtx);
}
if(empty()){
return nullptr;
}
Task_t res = _que.front();
_que.pop();
_notFull.notify_one();
return res;
}
// 判满
bool TaskQueue::full(){
return _que.size() == _capacity;
}
// 判空
bool TaskQueue::empty(){
return _que.size() == 0;
}
// 唤醒
void TaskQueue::wakeup(){
_flag = false;
_notEmpty.notify_all();
}
几点说明:
_flag是"线程池要退出了"的标志。pop()里的条件是empty() && _flag------只有"还没说要退出"的时候才继续等。wakeup()把_flag置 false 并notify_all,所有阻塞在pop()上的工作线程就会醒过来,走到if(empty()) return nullptr;,优雅退出。push用的是_notFull.wait(),也就是队列满的时候会阻塞住生产者。这一点在下一节"反压"里要重点聊。- 条件变量的
wait一定要写在while里而不是if里,防止虚假唤醒。
ThreadPool:固定线程数 + 任务队列
ThreadPool.h
cpp
#ifndef __THREADPOOL_H
#define __THREADPOOL_H
#include<thread>
#include<vector>
#include"TaskQueue.h"
using std::vector;
using std::thread;
using Task_t = function<void()>;
class ThreadPool{
public:
ThreadPool();
ThreadPool(size_t threadnum, size_t quesize);
~ThreadPool();
void start();
void stop();
void addTask(Task_t && ptask);
private:
Task_t getTask();
void doTask();
private:
size_t _threadNum;
vector<thread> _threads;
size_t _queSize;
TaskQueue _taskQue;
bool _isExit;
};
#endif
ThreadPool.cpp
cpp
/* ************************************************************************
> File Name: Thread_pool.cpp
> Author: hyj
> mail: 1683958261@qq.com
> Created Time: Mon 03 Aug 2026 07:39:37 PM CST
> Description:
************************************************************************/
#include"ThreadPool.h"
#include"logger.h"
ThreadPool::ThreadPool():ThreadPool(5, 10){}
// 构造
ThreadPool::ThreadPool(size_t threadnum, size_t quesize)
:_threadNum(threadnum)
,_threads()
,_queSize(quesize)
,_taskQue(_queSize)
,_isExit(false){
}
// 析构
ThreadPool::~ThreadPool(){}
// 启动
void ThreadPool::start(){
for(size_t idx = 0; idx < _threadNum; ++idx){
_threads.push_back(thread(&ThreadPool::doTask, this));
}
}
void ThreadPool::stop(){
// 表示退出不再接受新的任务
_isExit = true;
// 唤醒所有线程
_taskQue.wakeup();
// 等待子线程执行完毕
for(auto & th: _threads){
th.join();
}
}
void ThreadPool::addTask(Task_t && ptask){
if(ptask){
_taskQue.push(std::move(ptask));
}
}
Task_t ThreadPool::getTask(){
return _taskQue.pop();
}
void ThreadPool::doTask(){
while(!_isExit){
Task_t ptask = getTask();
if(ptask){
ptask();
}
else{
WARN_LOG("nullptr = ptask");
}
}
}
注意成员声明顺序:_threads、_taskQue 这些必须在初始化列表里对得上(_taskQue(_queSize) 用到了 _queSize,所以 _queSize 要写在 _taskQue 前面)。又是那个成员初始化顺序的坑。
EventLoop 的改造
这是 v4 的核心。先把改完的头文件整体贴一下,几个新增的点后面逐一说明。
EventLoop.h(完整)
cpp
#ifndef __EVENTLOOP_H
#define __EVENTLOOP_H
#include<sys/epoll.h>
#include"Acceptor.h"
#include"TcpConnection.h"
#include"ThreadPool.h"
#include<vector>
#include<map>
#include<memory>
#include<functional>
#include<mutex>
using std::vector;
using std::map;
using std::shared_ptr;
using std::function;
using std::mutex;
using TcpConnectionPtr = shared_ptr<TcpConnection>;
class TcpConnection;
class EventLoop{
public:
EventLoop(Acceptor & acc);
~EventLoop();
void loop();
void unloop();
// 注册回调
void setNewConnectionCallback(TcpConnectionCallback && cb);
void setMessageCallback(TcpConnectionCallback && cb);
void setCloseCallback(TcpConnectionCallback && cb);
// 存放任务并唤醒EventLoop(可以跨线程调用)
void runInLoop(Task_t && cb);
private:
void waitEpollfd();
void handleNewConnection();
void handleMessage(int fd);
int createEpollFd();
void addEpollReadFd(int fd);
void delEpollReadFd(int fd);
// 创建通知的文件描述符
int createEventFd();
// 读走通知,防止epoll_wait一直返回
void handleRead();
// 唤醒阻塞在epoll_wait上的线程
void wakeup();
// 执行挂起的任务
void doPendingFunctors();
private:
int _epfd; // epoll_create创建的文件描述符
vector<struct epoll_event> _evtList; // 存放就绪文件描述符的容器
bool _isLooping; // 是否在循环的标志
Acceptor & _acceptor; // Acceptor 引用
map<int, shared_ptr<TcpConnection>> _conns; // 存放描述符与TcpConnection连接的键值对
// 三个回调函数
TcpConnectionCallback _onNewConnection;
TcpConnectionCallback _onMessage;
TcpConnectionCallback _onClose;
// 任务容器
vector<Task_t> _pendings;
mutex _mutex;
int _evtfd;
};
#endif
1)数据成员:多三个
cpp
// 任务容器
vector<Task_t> _pendings; // 待执行的任务
mutex _mutex; // 保护 _pendings
int _evtfd; // eventfd,用于跨线程唤醒
为了用到 Task_t,EventLoop.h 要包含到 TaskQueue.h(这里是通过 ThreadPool.h 间接包含进来的),同时显式写一句 using std::mutex;------别指望别的头文件 using 过的名字,那属于"碰巧能编"。
2)新增函数
cpp
// 创建通知的文件描述符
int createEventFd();
// 读走通知,防止 epoll_wait 一直返回
void handleRead();
// 唤醒阻塞在 epoll_wait 上的线程
void wakeup();
// 执行挂起的任务
void doPendingFunctors();
// 存放任务并唤醒EventLoop
void runInLoop(Task_t && cb);
3)构造函数:创建并注册 _evtfd
先看头部的 include 变化:
cpp
#include"EventLoop.h"
#include"logger.h"
#include<sys/eventfd.h> // 新增:eventfd
#include<unistd.h> // 新增:read/write/close
#include<errno.h> // 新增:EINTR
#include<iostream>
#include<string.h>
#define LISTEN_CLIENT_MAX_NUM 1024
然后是构造函数和析构函数:
cpp
EventLoop::EventLoop(Acceptor & acc)
:_epfd(createEpollFd())
,_evtList(LISTEN_CLIENT_MAX_NUM)
,_isLooping(false)
,_acceptor(acc)
,_conns()
,_pendings()
,_mutex()
,_evtfd(createEventFd()){
// 把listenfd放到红黑树上
int listenfd = _acceptor.fd();
addEpollReadFd(listenfd);
// 把eventfd也放到红黑树上
addEpollReadFd(_evtfd);
}
EventLoop::~EventLoop(){
::close(_evtfd);
::close(_epfd);
}
这一步千万不能漏 :_evtfd 必须在构造时创建,并且注册进 epoll。否则 wakeup() 里 write 的是一个未初始化的 fd(大概率是 0 或者随机值),整个唤醒机制就是空转------任务永远等不到执行,代码看起来"能编译、能跑",但就是不回消息。这是这套机制里最容易踩的坑。
4)waitEpollfd:区分 eventfd,并在每轮末尾执行任务
cpp
void EventLoop::waitEpollfd(){
int nready = 0;
nready = epoll_wait(_epfd, _evtList.data(), _evtList.capacity(), 3000);
if(nready == -1){
if(errno == EINTR){
return ;
}
INFO_LOG("epoll_wait error");
return ;
}
else if(nready == 0){
INFO_LOG("epoll_wait timeout");
}
else{
// 处理文件描述符个数到达上限,触发扩容
if(nready == (int)_evtList.capacity()){
_evtList.resize(2 * nready);
}
for(int idx = 0; idx < nready; ++idx){
int listenfd = _acceptor.fd();
int fd = _evtList[idx].data.fd;
// 如果监听到listenfd则说明有新的连接
if(fd == listenfd){
handleNewConnection();
}
// 如果是eventfd,说明有别的线程在叫我们
else if(fd == _evtfd){
handleRead();
}
// 其它文件描述符说明有客户端发来消息
else{
handleMessage(fd);
}
}
}
// 无论是否超时,都执行一次挂起的任务
doPendingFunctors();
}
5)新函数的实现
cpp
int EventLoop::createEventFd(){
int fd = eventfd(0, 0);
if(fd < 0){
ERROR_LOG("eventfd error");
exit(EXIT_FAILURE);
}
return fd;
}
void EventLoop::wakeup(){
uint64_t u = 1;
ssize_t s = write(_evtfd, &u, sizeof(uint64_t));
if(s != sizeof(uint64_t)){
INFO_LOG("write eventfd error");
}
}
void EventLoop::handleRead(){
uint64_t u = 0;
ssize_t s = read(_evtfd, &u, sizeof(uint64_t));
if(s != sizeof(uint64_t)){
if(errno != EINTR){
ERROR_LOG("read eventfd error");
}
}
}
void EventLoop::runInLoop(Task_t && cb){
_mutex.lock();
_pendings.push_back(std::move(cb));
_mutex.unlock();
// 唤醒阻塞在epoll_wait上的IO线程
wakeup();
}
void EventLoop::doPendingFunctors(){
vector<Task_t> temp;
_mutex.lock();
temp.swap(_pendings);
_mutex.unlock();
for(auto & f : temp){
if(f){
f();
}
}
}
其余部分(loop / handleNewConnection / handleMessage / epoll 的三个封装 / 三个 setXXX)跟 v3 一模一样,只有两处小改动:
cpp
// loop 线程里创建的连接,要把自己的指针传进去
TcpConnectionPtr con(new TcpConnection(connfd, this));
// 退出时顺手唤醒一下,不用再等 epoll_wait 的 3 秒超时
void EventLoop::unloop(){
_isLooping = false;
wakeup();
}
这一块里几个必须注意的点
① handleRead() 一定要把 8 字节读干净。
eventfd 是水平触发 的语义:只要计数器不为 0,epoll_wait 就会一直返回它可读。read 一次(读出来的是累计计数)就会把计数器清零,fd 变回不可读,一切正常。反过来,如果你用小于 8 字节的缓冲区去读(eventfd 规定必须正好 8 字节,否则返回 EINVAL),或者干脆忘了读,计数器就一直不是 0,下一次 epoll_wait 立刻又返回这个 fd → 死循环 + 100% CPU,而且日志里什么都看不出来。
② 先入队、再唤醒,别反了。
runInLoop 里的顺序是 push_back → unlock → wakeup。虽然顺序反了最多只是"白唤醒一次"(醒来发现队列是空的,下一轮再看),但入队必须加锁 :_pendings 是被多条业务线程和 IO 线程同时访问的,不加锁就是数据竞争,会导致 vector 内部指针错乱(那种"崩在 std::vector 里、看栈看不出原因"的崩溃)。
③ doPendingFunctors 用 swap 而不是直接遍历。
如果直接在持锁状态下遍历 _pendings 并执行 f(),那么当某个 f() 内部又调用了 runInLoop(比如业务线程的逻辑嵌套、或者回调里再次投递),就会自己等自己的锁 → 死锁 。先 swap 到一个局部变量,立刻解锁,再在锁外执行------既把临界区缩到最短,又允许任务里再次投递。
④ _evtfd 谁都能唤醒它。
wakeup() 不是线程私有的,所以将来如果 EventLoop 要换线程、要退出,都能靠它把 IO 线程立刻叫醒。这就是 v3 里 stop() 要等 3 秒的问题的解法。
TcpConnection 的改造
TcpConnection 要多拿一个 EventLoop*,这样才能把发送任务投回去;同时新增 sendInLoop。
TcpConnection.h(改动部分)
cpp
class TcpConnection;
class EventLoop;
using TcpConnectionPtr = shared_ptr<TcpConnection>;
using TcpConnectionCallback = function<void(const TcpConnectionPtr &)>;
class TcpConnection
:public std::enable_shared_from_this <TcpConnection>{
public:
explicit TcpConnection(int fd, EventLoop *loop);
~TcpConnection();
string receive();
string toString();
bool isClosed();
void setNewConnectionCallback(const TcpConnectionCallback & cb);
void setMessageCallback(const TcpConnectionCallback & cb);
void setCloseCallback(const TcpConnectionCallback & cb);
void handleNewConnectionCallback();
void handleMessageCallback();
void handleCloseCallback();
void send(const string & msg);
// 跨线程投递:把发送动作交回IO线程
void sendInLoop(const string & msg);
private:
InetAddress getLocalAddr();
InetAddress getPeerAddr();
private:
SocketIO _sockIO;
Socket _sock;
InetAddress _localAddress;
InetAddress _peerAddr;
TcpConnectionCallback _onNewConnection;
TcpConnectionCallback _onMessage;
TcpConnectionCallback _onClose;
EventLoop *_loop; // 指向自己所属的EventLoop
};
TcpConnection.cpp(改动部分)
cpp
#include "TcpConnection.h"
#include<sstream>
#include"logger.h"
#include "EventLoop.h"
using std::ostringstream;
using std::bind;
TcpConnection::TcpConnection(int fd, EventLoop *loop)
:_sockIO(fd)
,_sock(fd)
,_localAddress(getLocalAddr())
,_peerAddr(getPeerAddr())
,_loop(loop){
}
void TcpConnection::sendInLoop(const string & msg){
_loop->runInLoop(bind(&TcpConnection::send, shared_from_this(), msg));
}
sendInLoop 里传给 runInLoop 的是一个"待执行的发送任务":把 msg 拷贝一份、绑定到 TcpConnection::send 上,扔回 IO 线程。业务线程调完就可以去干别的了。
这里用 shared_from_this() 而不是 this,是关键。
任务从"投递"到"执行"之间有一段时间差,而在这段时间里:
- IO 线程可能已经判定对端关闭,把
_conns里的shared_ptrerase掉了; - 如果
bind的是裸this,任务执行时对象可能已经析构 ------ 悬垂指针,send里访问_sockIO直接崩。
传一个 shared_ptr 进去,就把这条连接的引用计数延长到了任务执行完毕;只要业务线程还持有 TcpConnectionPtr,这条连接就不会被真正销毁 ,fd 也不会被提前关掉。这也是 TcpConnection 要从 enable_shared_from_this 继承的原因。
main.cpp:把业务丢进线程池
cpp
#include"TcpServer.h"
#include"ThreadPool.h"
#include<iostream>
using std::cout;
using std::endl;
using std::bind;
ThreadPool *gpool = nullptr;
class MyTask{
public:
MyTask(const string & str, const TcpConnectionPtr & con)
:_msg(str)
,_con(con){
}
void process(){
// 这里才是真正的"业务处理",可以放心地耗时
_msg = "msg:" + _msg;
// 算完了不能直接发,要交回IO线程发
_con->sendInLoop(_msg);
}
private:
string _msg;
TcpConnectionPtr _con;
};
void onNewConnection(const TcpConnectionPtr & con){
cout << con->toString() << " connected!" << endl;
}
void onMessage(const TcpConnectionPtr & con){
string msg = con->receive();
cout << "recv msg from client: " << msg << endl;
// 别在这里做业务,丢给线程池
MyTask task(msg, con);
gpool->addTask(bind(&MyTask::process, task));
}
void onClose(const TcpConnectionPtr & con){
cout << con->toString() << " closed " << endl;
}
int main()
{
ThreadPool pool(4, 10);
gpool = &pool;
pool.start();
TcpServer server("0.0.0.0",8080);
server.setAllCallback(onNewConnection,onMessage,onClose);
server.start(); // 阻塞在事件循环里
pool.stop();
return 0;
}
对照一下 v3 的 onMessage:
cpp
// v3:IO线程里串行执行
void onMessage(const TcpConnectionPtr & con){
string msg = con->receive();
msg = "msg:" + msg;
con->send(msg); // 自己算、自己发
}
cpp
// v4:IO线程只负责收,算和发都安排好了
void onMessage(const TcpConnectionPtr & con){
string msg = con->receive(); // 收:IO线程
MyTask task(msg, con);
gpool->addTask(bind(&MyTask::process, task)); // 算:线程池;发:回投IO线程
}
注意 onMessage 里千万不要再出现 con->send(...)。如果业务处理完还是在 IO 线程里同步发送,那线程池就白加了------等于只把"计算"挪走了,而"计算 + 发送"还是串行在 IO 线程上。
回调是普通函数,拿不到
ThreadPool对象,所以这里先用一个全局指针gpool顶一下。更好的写法是用 lambda 捕获、或者干脆把业务逻辑连同线程池一起封装成一个类(下一版要做的事)。
编译与运行
bash
g++ *.cpp -o server -std=c++11 -llog4cpp -lpthread
./server
开两个终端,同时发起连接:
bash
# 终端1、终端2
nc 127.0.0.1 8080
每个终端敲一句话,两个连接都会立刻收到回复,不会互相阻塞:
>>recv msg from client: aaa
>>recv msg from client: bbb
【图 3:多客户端并发测试截图】
【图 4:日志截图 ------ 可以看到处理同一条
TcpConnection的线程 id 与 IO 线程不同】
想更直观地验证"计算不阻塞 IO",可以在 MyTask::process() 里加一行 sleep(2);:这时再开两个终端,你会看到两句话的回复几乎同时在两秒后到达,而不是一个两秒、再一个两秒。这就是并发和串行的区别。
两个要留神的地方
1)队列满会"反压"到反应堆
TaskQueue::push 在队列满时是阻塞 的,而 push 的调用方是 onMessage------跑在 IO 线程上。也就是说:
业务处理得越慢 → 队列越容易满 → IO 线程被卡在
push里 → 反应堆停摆。
我们绕了一大圈把计算从 IO 线程挪走,结果通过一条"满队列"的路径又把 IO 线程卡住了。这是很隐蔽的一个坑,处理方式有几种:
- 让
push支持"非阻塞 + 队列满时返回失败",在onMessage里做丢弃 / 回一个"服务器忙"; - 给不同优先级的任务分不同队列;
- 业务压测之后按经验把队列容量调大(治标)。
2)_isExit 是个普通 bool
ThreadPool::_isExit 会被主线程写、被工作线程读,严格来说是个数据竞争,加个 std::atomic<bool> 更规范(实际运行中因为有条件变量的通知兜着,一般不会出问题,但没必要赌)。另外 ThreadPool::stop() 里的 join() 只是等线程结束,队列里还没执行完的任务是会被丢掉的 ------如果要"优雅退出",得让 pop() 在 _flag 为 false 时继续把队列里剩下的任务执行完再退出。
小结
v4 做的事情,一句话概括:把 IO 线程和计算线程分开,并且给它们之间搭了两条可靠的路。
- IO → 计算 :
onMessage里把业务打包成任务丢进TaskQueue,线程池pop出来执行。 - 计算 → IO :
sendInLoop→EventLoop::runInLoop→ 入队 +eventfd唤醒 → IO 线程执行发送。
两个方向的桥都是 std::function<void()> + 一个线程安全队列,这个模式在 muduo 里就是 runInLoop / queueInLoop,是绝大多数 C++ 网络库的骨架。
接下来的问题:
- 连接管理的瓶颈还在 。所有连接都跑在同一个
EventLoop上,也就是一条 IO 线程。连接数上万、或者单连接 IO 很重(传大文件)的时候,单条 IO 线程仍然是瓶颈。这就是**主从 Reactor(Main-Reactor + Sub-Reactor)**要解决的问题------Acceptor 所在的 Main Reactor 只负责 accept,然后按 round-robin 把新连接分给若干 Sub Reactor,每个 Sub Reactor 一条线程、一个epoll。 - 发送仍然是阻塞的 。
writen在对端接收窗口满的时候会阻塞 IO 线程。彻底的解法是把连接改成非阻塞,加一个应用层输出缓冲区,注册EPOLLOUT,"发不完就先存着,等可写事件再发"------也就是 TCP 三个半事件里剩下那"半个事件"。
这两件事加上把业务逻辑封装成一个干净的 EchoServer 类(去掉全局变量 gpool),就是 v5 的内容了。
`。
- 发送仍然是阻塞的 。
writen在对端接收窗口满的时候会阻塞 IO 线程。彻底的解法是把连接改成非阻塞,加一个应用层输出缓冲区,注册EPOLLOUT,"发不完就先存着,等可写事件再发"------也就是 TCP 三个半事件里剩下那"半个事件"。
这两件事加上把业务逻辑封装成一个干净的 EchoServer 类(去掉全局变量 gpool),就是 v5 的内容了。