⭐️在这个怀疑的年代,我们依然需要信仰。
个人主页 :YYYing.
⭐️C++大型项目系列专栏:C++大型项目之高性能服务器框架
系列上期内容:【C++项目之高性能服务器框架 (二) 】线程&协程篇
系列下期内容:暂无
目录
前言:
一、故事背景
二、第一版:单线程 while 循环 —— 连 100 人都撑不住
2.1 怎么写的?
2.2 出了什么问题?
2.3 当时的感受
三、第二版:一个连接一个线程 —— 内存炸了
3.1 怎么改的?
3.2 出了什么问题?
3.3 当时的感受
四、第三版:引入协程,但没有调度器 —— 噩梦刚刚开始
4.1 怎么改的?
4.2 出了什么问题?
4.3 当时的感受
五、第四版:有了调度器,但 IO 还是阻塞 —— 数据库一查,全服卡顿
5.1 怎么改的?
5.2 出了什么问题?
5.3 你尝试的非阻塞方案
5.4 你尝试的 epoll + 回调方案
5.5 当时的感受
六、第五版:有了 IOManager —— 但僵尸连接吃光内存
6.1 怎么改的?
6.2 出了什么问题?
6.3 当时的感受
七、最终版:三者合体,才是完全体
7.1 场景:排行榜查询(带超时 + 非阻塞 IO + 调度)
7.2 场景:僵尸连接清理
7.3 场景:心跳检测
7.4 场景:技能 CD
八、如果没有这三个组件,系统会怎样?
没有 Scheduler
没有 TimerManager
没有 IOManager
三者都缺
九、总结
十、学习验证清单
结语
前言:
本项目是基于小电视里sylar大佬的项目来做的一个项目总结,其多为一些项目思考与笔记,可能还会有一些图解之类的讲解,但光看本专栏学习此项目肯定是不足的,多去跟着视频敲敲代码或者自己下去实现实现各个模块。由于小生经验不足,这个系列专栏制作周期可能会稍微有点长,甚至有可能会出现断更的情况,但我尽量往完写,望各位大佬多多包涵。
但在开始讲今天的设计之前,我想先说两句最近的思考。
我们从0开始做项目一般都不应该仅仅只是去做一个一成不变的东西,也就是说我们是需要迭代、需要针对场景的,那么在这个过程中,我们项目往往就需要解决一些实际的具体问题,比如说我们之前的日志系统:
如果我们只需要跑动一个简单的服务器那么我们真的需要这个日志系统吗?想来必然是不需要的,那么我们为什么要去做这个东西?因为我们需要在运行的过程中去监控一些我们的运作信息,不仅仅是为了我们能运行、监控它,更是用户也需要通过日志来验证到底是哪出了问题等等;还有,我们同步日志系统在高并发运作的时候,往往也会有一些性能瓶颈,比如磁盘I/O慢,锁竞争激烈,日志量大时阻塞业务线程,也没办法批量写,这也是sylar日志系统暂时存在的问题,那么我们就可以迭代为双缓冲异步日志系统(如有兴趣小电视就可以搜到此处不作赘述)。
上一次的线程与协程也是如此,那么我们今天要讲的协程调度器,定时器与IO协程管理器更是如此。所以今天不同以往,我们就先来探究探究是因为会在哪些具体的场景里踩坑,才会逼着我们把这三个东西一个一个做出来(当然,后面如有需要我也会按照这个风格去讲)。
一、故事背景
假设你现在接手了一个项目:游戏网关服务器(Game Gateway)。
它的职责很简单:
接受客户端(手机/PC 游戏)的 TCP 长连接。
转发玩家的请求到后端逻辑服(战斗、背包、聊天等)。
把后端的响应回传给对应的玩家。
管理连接生命周期:心跳检测、空闲超时、异常断开。
项目一开始只有几百人在线,但老板的目标很明确:上线后要达到 10 万同时在线。
下面我们就跟着这个项目一路踩坑,看看为什么最后必须把 Scheduler(协程调度器)、TimerManager(定时器)、IOManager(IO协程管理器) 全部做出来。
二、第一版:单线程 while 循环 —— 连 100 人都撑不住
2.1 怎么写的?
项目赶工期,你写了一个最直观的服务器:
// 第一版:单线程循环处理所有连接 int listen_fd = socket(...); bind(listen_fd, ...); listen(listen_fd, 128); while (running) { int fd = accept(listen_fd, ...); // 阻塞! // 新玩家连上来了 handle_player(fd); // 处理这个玩家——还是阻塞! }handle_player里面:
void handle_player(int fd) { while (true) { char buf[1024]; int n = read(fd, buf, sizeof(buf)); // 阻塞! if (n <= 0) break; process_request(buf, n); // 可能还要访问数据库?也是阻塞! write(fd, response, len); // 阻塞! } close(fd); }2.2 出了什么问题?
场景:玩家 A 在加载大地图资源
玩家 A 点击"进入副本",客户端向网关发送一个 2MB 的资源请求。网关需要把这 2MB 请求转发给资源服,等资源服处理完再返回。
// 网关转发请求到资源服 send_to_resource_server(req); char response[1024 * 1024 * 2]; int n = read(resource_fd, response, sizeof(response)); // 阻塞 3 秒!在read(resource_fd)这 3 秒里:
玩家 B 想登录?等着,
accept在循环里,A 没处理完根本轮不到 B。玩家 C 想发个聊天消息?等着,
handle_player卡在 A 的请求里。整个服务器被一个玩家卡死,其他所有人都在排队。
这就是单线程阻塞模型的致命伤:一个请求慢,全部请求陪葬。
2.3 当时的感受
"卧槽,玩家 A 进个副本,全服都登不上去了?"
三、第二版:一个连接一个线程 —— 内存炸了
3.1 怎么改的?
你很快想到:多线程啊!每个玩家一个线程,互不干扰:
while (running) { int fd = accept(listen_fd, ...); std::thread([fd]() { // 每个连接开一个线程! handle_player(fd); }).detach(); }3.2 出了什么问题?
场景:在线人数冲到 5000
游戏火了,同时在线 5000 人。我们打开top一看:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 1234 root 20 0 120g 80g ... ... ... ... gateway5000 个线程,每个线程栈默认 8MB,光是栈就占40GB 虚拟内存。
物理内存(RSS)也有 80GB,因为每个线程的栈虽然不能全用,但内核要分配页表,还要维护线程控制块(TCB)。
CPU 使用率不高,但%si(软中断)爆表,时间全花在线程切换上了。
更痛苦的场景:凌晨 3 点,运维打电话来
"服务器又 OOM 了!今天第 3 次!"
一查日志,发现连接数只有 8000,但内存已经 96GB 撑满。你想:"我没申请多少内存啊?"
问题不在我们的代码上,而在pthread上。每个线程的栈空间、TLS、调度器数据结构,累加起来就是天文数字。
这就是 C10K 问题的本质:线程是有重量的,而连接数可以无限增长。
3.3 当时的感受
"一个连接一个线程,代码是简单了,但 1 万个玩家就要 1 万个线程?服务器直接被压垮。" "有没有一种东西,像线程一样能独立执行,但又像普通对象一样轻量?"
四、第三版:引入协程,但没有调度器 —— 噩梦刚刚开始
4.1 怎么改的?
这时候你听说了"协程"这个东西:用户态轻量级线程,切换不需要进内核,占用的栈可以很小(比如 128KB),一个线程上可以跑几千个协程。
于是你引入了 sylar 的Fiber,改成这样:
// 主线程里,为每个连接创建一个协程 void on_new_connection(int fd) { Fiber::ptr f(new Fiber([fd]() { handle_player(fd); })); f->swapIn(); // 切换到协程执行 }handle_player里面也用协程的方式:
void handle_player(int fd) { while (true) { char buf[1024]; int n = read(fd, buf, sizeof(buf)); // 数据没到?阻塞! // ... } }4.2 出了什么问题?
问题 1:协程阻塞了,整个线程还是冻住
协程里调了read(),这是系统调用,会阻塞整个线程。一个线程上跑了 1000 个协程,其中一个协程阻塞了,另外 999 个协程全停摆。
协程本身没有解决阻塞问题,它只是"能切换",但你得知道什么时候该切换、切换完谁去执行别的。
问题 2:没有调度器,新连接来了谁处理?
你开了 4 个线程,每个线程上跑 1000 个协程。主循环大概长这样:
// 第三版:没有调度器,手动管理协程 std::vector<Fiber::ptr> fibers; // 所有协程放这里 void thread_main() { while (running) { for (auto& f : fibers) { if (f->getState() == Fiber::HOLD) { // 这个协程之前让出了,现在要不要恢复? // 怎么判断它等待的事情(比如IO)好了没? // 只能轮询!每个协程问一遍:"你好没?" } } // 新连接怎么加入?加锁往 fibers 里 push_back // 但 4 个线程都在遍历 fibers,加锁竞争严重 } }场景:新玩家 E 登录
玩家 E 的 TCP 连接建立,accept返回了 fd。现在你要把 E 的处理协程交给某个线程执行。
但你发现:
线程 1 上 1000 个协程全阻塞在数据库查询。
线程 2 上 1000 个协程全阻塞在资源服请求。
线程 3 上有个协程刚让出 CPU,但它让出之后,线程 3 怎么知道该执行别的协程?
没有中央调度器 = 没有"任务分发系统"。
每个线程各自为政,新任务来了不知道该给谁。负载严重不均衡,有的线程忙死,有的线程闲死。
问题 3:协程让出后,谁来唤醒?
玩家 F 发了一条聊天消息,协程处理到一半,需要等数据库返回结果。协程YieldToHold()让出了。
数据库结果回来了。谁负责找到玩家 F 对应的协程,把它重新放入可执行队列?
没有调度器,你只能这样写:
// 数据库结果回来的回调里 void on_db_result(uint64_t player_id, Data data) { // player_id -> 找到协程 -> 恢复执行 // 但协程在哪个线程上?怎么安全地恢复? // 加锁遍历所有线程的所有协程?性能爆炸。 }4.3 当时的感受
"协程是好东西,但用起来比线程还麻烦。" "我写了 200 行主循环逻辑,就为了决定'接下来该执行哪个协程',这代码根本维护不了。" "我需要一个'秘书',帮我把任务分配给线程,协程让出了自动重新排队。"
这就是 Scheduler 的诞生背景。
五、第四版:有了调度器,但 IO 还是阻塞 —— 数据库一查,全服卡顿
5.1 怎么改的?
你实现了Scheduler:
Scheduler scheduler(4, true, "gateway"); // 4 个线程 void on_new_connection(int fd) { scheduler.schedule([fd]() { handle_player(fd); // 把这个任务交给调度器 }); }调度器内部有一个m_fibers队列,4 个线程竞争取任务,负载均衡了。协程让出后会重新入队,不需要你手动管理。
5.2 出了什么问题?
场景:排行榜查询
玩家 G 点击"查看全服排行榜",请求到网关。网关需要查 MySQL:
// 在协程里执行的代码 void handle_rank_request(int fd) { send_to_db("SELECT * FROM rank ORDER BY score DESC LIMIT 100"); char result[10240]; int n = read(db_fd, result, sizeof(result)); // 数据库查询花了 2 秒! // read 阻塞了整个线程! write(fd, result, n); }在read(db_fd)这 2 秒里:
这个线程上还有 50 个其他协程在等待执行。
这 50 个协程里,有玩家 H 的登录请求、玩家 I 的购买道具请求、玩家 J 的聊天消息。
全被卡住了。玩家 H 点了登录,界面转圈 2 秒才进去。
场景:资源服过载
游戏有个功能是"下载玩家头像",头像存在资源服。资源服今天网络抖动,响应从 50ms 变成了 500ms。
网关向资源服请求头像:
int n = write(resource_fd, req, len); // 发请求 char buf[1024 * 64]; int n = read(resource_fd, buf, sizeof(buf)); // 等 500ms网关开了 4 个线程,每个线程处理大量协程。资源服一慢,4 个线程上所有等待资源服的协程都把线程阻塞了。
结果:
实际上只有 10% 的请求是访问资源服的。
但 100% 的请求都变卡了,因为线程被阻塞,调度器无线程可用。
这就是"一个协程的阻塞 IO 拖垮整个调度器"的问题。
5.3 你尝试的非阻塞方案
你想到:把 fd 设为非阻塞不就行了?
fcntl(fd, F_SETFL, O_NONBLOCK);于是代码变成:
void handle_rank_request(int fd) { send_to_db("SELECT ..."); char result[10240]; while (true) { int n = read(db_fd, result, sizeof(result)); if (n > 0) break; if (errno == EAGAIN) { // 数据还没到... 怎么办? // 方案 A:忙等 // continue; // CPU 100% // 方案 B:sleep usleep(1000); // 睡 1ms,精度差,响应慢 continue; // 方案 C:yield,但谁来唤醒? // Fiber::YieldToHold(); // 然后... 没人唤醒这个协程,它永远沉睡 } } }方案 A(忙等):CPU 飙到 100%,服务器变成了电暖器。
方案 B(sleep):每次 sleep 1ms,累积下来响应延迟巨大。而且 sleep 期间这个线程不能干别的(虽然没阻塞内核,但也没在干活)。
方案 C(yield):协程让出了,但没有机制在数据准备好时唤醒它。协程永远沉睡在HOLD状态,直到服务器重启。
5.4 你尝试的 epoll + 回调方案
你学过 C++系统编程,知道 epoll。于是你写了一套 epoll + 回调的机制:
// epoll 告诉你 fd 可读了,回调这个函数 void on_db_fd_readable(int db_fd) { // 但此时你根本不知道这个 fd 对应的是哪个玩家的哪个请求! // 你得自己维护一个 map:fd -> player_id -> request_state // ... }一个排行榜查询的处理逻辑,被你拆成了 4 个回调:
on_connect:连接数据库on_db_writable:发送查询请求on_db_readable:读取查询结果on_response_sent:把结果写给玩家
原本 20 行的同步逻辑,变成了分布在 4 个文件里的 200 行回调。调试的时候,你在日志里看到报错,但根本不知道这个回调属于哪个玩家、哪个请求。
这就是"回调地狱"(Callback Hell)。
5.5 当时的感受
"协程让出后没人唤醒,这跟协程没用有什么区别?" "epoll 是好,但回调写法太恶心了,一个请求拆成 5 个函数,Debug 要命。" "我就想写
read(fd, buf),数据没到自动挂起,数据到了自动继续,很难吗?"
这就是 IOManager 的诞生背景。
六、第五版:有了 IOManager —— 但僵尸连接吃光内存
6.1 怎么改的?
你实现了IOManager,它把 epoll、调度器、协程三者粘合在一起。
现在你的代码看起来终于正常了:
void handle_rank_request(int fd) { send_to_db("SELECT ..."); char result[10240]; int n = read(db_fd, result, sizeof(result)); // 看起来是阻塞 read! // 实际上:数据没到 → 协程自动挂起 → 线程去干别的 // 数据到了 → epoll 通知 → 协程自动恢复 → 从这行继续执行 write(fd, result, n); }一个请求的处理逻辑,完整地写在一个函数里,没有回调,没有碎片化。
6.2 出了什么问题?
场景 1:玩家手机断网,服务器一无所知
玩家 K 在地铁里玩游戏,进隧道了,手机没网。但玩家 K 的 TCP 连接还在服务器上挂着。
为什么?因为 TCP 连接不会自动感知对端断网。除非对端发 RST/FIN 包,或者服务器尝试发数据收到超时。
玩家 K 在隧道里待了 5 分钟,出隧道后重新登录,新建了一个连接。但老连接还在服务器上。
老连接占用一个 fd。
老连接占用一个协程对象(128KB 栈)。
老连接占用一个
FdContext。
如果每天有 10% 的玩家经历这种"闪断重连",10 万在线就意味着有 1 万个僵尸连接。
一周后,运维又打电话来:
"服务器 fd 耗尽了!
ulimit -n设的 65535,但全用完了!"
你去查:lsof -p <pid> | wc -l,65535。其中 60% 是TCP ESTABLISHED,但这些连接已经几个小时没数据了——它们的主人早就重新登录了。
没有定时器,你无法做空闲连接超时。
场景 2:排行榜查询又出事了
排行榜查询的 SQL 语句没加索引。今天全服冲榜,数据库压力暴增。
玩家 L 点了排行榜,handle_rank_request协程开始执行,向数据库发请求。数据库 30 秒后才返回。
在这 30 秒里:
玩家 L 的协程一直存在,占着 128KB 栈内存。
玩家 L 的 TCP 连接一直存在,占着一个 fd。
玩家 L 可能早就关掉排行榜界面了,但服务器还在傻等数据库。
如果同时有 100 个玩家点了排行榜,就是100 个协程 × 30 秒 × 128KB = 12.8MB 内存被白白占用。而且这 100 个协程持有的数据库连接也被占着,新的查询进不来。
没有定时器,你无法做请求超时。
场景 3:心跳检测怎么做?
你的协议规定:客户端每 30 秒发一个心跳包,服务端如果 90 秒没收到心跳,就断开连接。
没有定时器,你只能在主循环里写:
while (running) { for (auto& conn : all_connections) { if (now() - conn.last_heartbeat > 90000) { close(conn.fd); } } sleep(1); }10 万个连接,每秒遍历一次,就是 10 万次比较。
sleep(1)精度只有 1 秒,实际上第 89 秒该断的,可能拖到第 90 秒才断。你想做毫秒级超时?不可能。
连接数涨到 100 万时,这遍历循环本身就是性能瓶颈。
场景 4:游戏技能 CD
玩家 M 放了一个大招,技能冷却 5 秒。5 秒内不能再次释放。
这个"5 秒后允许释放"怎么实现?
// 没有定时器的丑陋写法 uint64_t last_cast_time = now(); void on_player_cast_skill() { if (now() - last_cast_time < 5000) { return "技能冷却中"; } // 放技能... last_cast_time = now(); }这个写法能工作,但它是"轮询式"的:每次玩家点技能都要检查一次。如果你有 1000 个技能的 CD 要管理,你不可能为每个技能写一个last_cast_time变量,然后在某个地方统一轮询。
你需要的是:5 秒后自动触发一个回调,把技能状态设为可用。
6.3 当时的感受
"连接超时、请求超时、心跳检测、技能 CD、延迟重试……" "到处都是'过一段时间再做某事',但我没有任何工具能优雅地管理这些定时任务。" "我总不能每秒遍历 10 万个连接检查超时吧?"
这就是 TimerManager 的诞生背景。
七、最终版:三者合体,才是完全体
现在你把 Scheduler、TimerManager、IOManager 都实现了。让我们看看同样的场景,现在怎么处理。
7.1 场景:排行榜查询(带超时 + 非阻塞 IO + 调度)
void handle_rank_request(int fd) { // 1. 注册一个 5 秒超时的定时器 auto timer = IOManager::GetThis()->addTimer(5000, [fd]() { // 5 秒后数据库还没回来,强制返回超时 send_error(fd, "查询超时"); close_connection(fd); }); // 2. 发送数据库请求 send_to_db("SELECT ..."); // 3. 读取结果——同步写法,异步执行 char result[10240]; int n = read(db_fd, result, sizeof(result)); // 数据没到?协程自动 yield,线程去处理别的玩家 // 数据到了?epoll 唤醒,协程恢复,继续执行这行 // 4. 数据库回来了,取消超时定时器 timer->cancel(); // 5. 返回结果 write(fd, result, n); }发生了什么?
| 阶段 | 没有三个组件 | 有了三个组件 |
|---|---|---|
| 发请求 | read阻塞 2 秒,线程上 50 个协程全卡住 | 数据没到 →YieldToHold()→ 线程去服务别人 |
| 数据回来 | 线程阻塞结束,read返回 | epoll 触发 →triggerEvent→schedule→swapIn恢复 |
| 数据库卡了 | 协程傻等 30 秒,占内存占连接 | 5 秒定时器触发,强制断开,释放资源 |
| 代码写法 | 回调地狱,4 个回调函数 | 一个函数写完,像同步代码 |
7.2 场景:僵尸连接清理
// 连接建立时,注册一个 30 秒空闲超时定时器 auto idle_timer = iomanager->addTimer(30000, [fd]() { close_connection(fd); log("fd=%d 空闲 30 秒,自动断开", fd); }, false); // 每次收到数据,刷新定时器 void on_data_received(int fd) { idle_timer->refresh(); // 重新计时 30 秒 process_packet(...); }玩家断网重连?老连接 30 秒后自动断开,fd 被回收。
不再轮询 10 万个连接,
refresh()是 O(log N)。精度是毫秒级的,不是
sleep(1)的秒级。
7.3 场景:心跳检测
// 每 30 秒检查一次心跳 auto heartbeat_timer = iomanager->addConditionTimer(30000, [fd]() { if (now() - last_heartbeat_time > 90000) { close_connection(fd); } }, weak_ptr_to_connection, true); // recurring = true,循环执行weak_ptr_to_connection:如果连接对象已经被销毁(比如正常断开了),定时器回调不会执行。recurring = true:每 30 秒自动触发一次,不需要手动重新注册。
7.4 场景:技能 CD
void on_cast_skill(Player* player) { if (player->skill_cooling) { return "技能冷却中"; } player->skill_cooling = true; // 5 秒后自动解除冷却 iomanager->addTimer(5000, [player]() { player->skill_cooling = false; }); // 执行技能... }不需要轮询,不需要
last_cast_time。5 秒后自动执行回调,代码干净、准确、不浪费 CPU。
八、如果没有这三个组件,系统会怎样?
没有 Scheduler
真实场景:游戏开新服,5000 人同时涌入 问题表现: ├─ 你开了 4 个线程处理连接,但线程 1 上的玩家全在查数据库(阻塞) ├─ 线程 2、3、4 上的协程闲着,但新连接被分到了线程 1 ├─ 玩家投诉:"为什么我朋友能进,我一直卡加载 90%?" ├─ 你在主循环里写了 300 行 if-else 来手动分配任务,Bug 修不完 └─ 最后被迫改成"一个连接一个线程",内存又炸了没有 TimerManager
真实场景:线上运行 7 天 问题表现: ├─ fd 耗尽,新玩家无法登录 ├─ 查日志发现 40% 的连接 6 小时没数据(玩家早闪退了) ├─ 数据库慢查询堆积,因为没有请求超时机制 ├─ 内存缓慢增长,因为僵尸协程永远不会释放 ├─ 运维写了个 crontab 每天凌晨 3 点重启服务器(传说中的"定时重启保平安") └─ 玩家投诉:"怎么每天凌晨都掉线?"没有 IOManager
真实场景:任何涉及 IO 的操作 问题表现: ├─ 玩家查排行榜,全服卡顿 2 秒 ├─ 玩家下载头像,资源服一抖,网关线程全冻结 ├─ 你改成非阻塞,但 EAGAIN 后不知道怎么办 ├─ 你改成 epoll + 回调,一个请求拆成 5 个函数,Debug 想死 ├─ 新同事入职,看了 3 天代码,说"这回调链我跟不进去" └─ 协程变成了摆设:能切换,但切换完没人管三者都缺
这就是阻塞多线程服务器的最终形态:
真实场景:10 万同时在线的目标 结果: ├─ 一个连接一个线程 → 10 万个线程 ├─ 每个线程 8MB 栈 → 80GB 虚拟内存 ├─ 线程切换开销 → CPU 80% 时间在切线程 ├─ 阻塞 IO → 线程大部分时间闲着等网络/数据库 ├─ 没有超时 → 内存泄漏 + 连接堆积 ├─ 没有心跳 → 僵尸连接吃光 fd └─ 实际能支撑的并发:几千人(C10K 瓶颈)九、总结
| 组件 | 它是在哪个真实场景里被逼出来的? | 没有它,你会在凌晨 3 点接到什么电话? |
|---|---|---|
| Scheduler | 一个玩家卡死全服、线程负载不均、协程让出后没人管 | "新服开了,玩家说有人能进有人进不去,是不是你的负载均衡有问题?" |
| TimerManager | fd 耗尽、数据库慢查询拖垮全服、每天凌晨必须重启服务器 | "服务器又 OOM 了!今天第 3 次!你那个遍历 10 万连接的超时检查能不能优化?" |
| IOManager | 数据库一查全服卡顿、非阻塞 IO 不会写、epoll 回调地狱 | "查个排行榜全服卡 2 秒?还有你那代码,一个请求拆 5 个回调,我怎么 Debug?" |
记住这三个画面:
Scheduler:秘书把任务分配给几个员工,没任务时员工休息,有任务时自动唤醒。
TimerManager:厨房里每个锅都有个定时器,到点了自动响,不用厨师每隔一秒抬头看一次。
IOManager:服务员等上菜时可以去服务别的桌,菜好了厨房自动喊他回来——而不是傻站着等,也不是满餐厅跑来跑去看哪个菜好了。
十、学习验证清单
读完这个故事,你应该能:
- 向一个没写过服务器的人讲清楚:"一个连接一个线程"为什么撑不到 1 万人。
- 描述"回调地狱"的具体痛苦:一个请求被拆成几个回调,Debug 时失去了什么信息?
- 解释为什么协程本身不够——有了协程,为什么还要 IOManager?
- 说出 3 个没有定时器会导致线上事故的具体场景。
- 说明 tickle 管道在真实业务中的作用:如果所有线程都在
epoll_wait,新用户登录的请求谁来处理? - 比较"遍历 10 万个连接检查超时"和"定时器 set"在复杂度和精度上的差异。
结语
我相信各位看完本篇博客对于这三个东西是怎么来的也是非常通透了,当然此处还有很多项目优化值得我们拿来说一说比如百万并发的时候怎么优化;定时器是set,那么这个数据结构能不能优化,这些后续有机会的话也会拿来说说。那么下一次博客就该我们这三个东西的代码环节了,希望这个专栏能帮到你。
我是YYYing,后面还有更精彩的内容,希望各位能多多关注支持一下主包。
无限进步,我们下次再见!