137 / 163 · C11 · 约 8 分钟
基于事件的并发(进阶)
先记住这句话
本章介绍不依赖线程构建并发服务器的方法。程序围绕事件循环运行,一次只处理一个到达的事件,从而获得对调度的完全控制并消除锁的需求。关键限制是处理程序绝不能执行会阻塞的操作。
事件循环如何工作
服务器进入一个永不退出的循环,每次迭代先收集当前待处理的事件,再依次调用对应的处理函数。处理函数只做少量工作便立即返回,循环因此始终掌握下一步该执行什么。开发者由此直接决定任务顺序,而不把决定权交给操作系统调度器。
用select监视多个描述符
select让程序一次性询问一组文件描述符是否已准备好读、写或出现异常。调用者填入感兴趣的描述符集合并可选地给出超时。返回时集合被改写成仅含真正就绪的描述符,循环随后只处理这些描述符,既不会空转也不会意外阻塞。
单线程执行消除锁
整个程序只有一条执行流,任何时刻最多只有一个处理函数在运行。共享数据因此不可能被并发访问,互斥锁、条件变量以及它们带来的死锁和竞争条件全部消失。程序员可以专注于业务逻辑而不必再为同步细节分心。
阻塞调用会冻结整个循环
一旦某个处理函数发出需要等待磁盘或网络的系统调用,循环就会停在那里,后续到达的事件全部得不到服务。保持响应性的唯一办法是让所有I/O都采用立即返回的非阻塞接口,真正的等待由操作系统在后台完成后再以新事件通知循环。
常见误区
- 在处理函数里调用会阻塞的open或read会让整个服务器失去响应
- select返回后没有逐一检查所有就绪描述符会漏掉事件
运行一个例子
最低标准 C11 · 完整程序 · 下载 .c
#include <stdio.h>
#include <stdbool.h>
int main(void) {
bool ready[2][4] = {
{true, false, true, false},
{false, true, false, true}
};
const char *names[4] = {"listen", "clientA", "clientB", "disk"};
printf("Starting simulated event loop\n");
for (int round = 0; round < 2; round++) {
printf("Round %d:\n", round + 1);
for (int fd = 0; fd < 4; fd++) {
if (ready[round][fd]) {
printf(" Handling event on %s (fd %d)\n", names[fd], fd);
}
}
}
printf("Event loop finished without blocking\n");
return 0;
}
在本地编译
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror ostep-33-event-based.c -o example && ./example预期结果
Starting simulated event loop
Round 1:
Handling event on listen (fd 0)
Handling event on clientB (fd 2)
Round 2:
Handling event on clientA (fd 1)
Handling event on disk (fd 3)
Event loop finished without blocking
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
为什么单CPU上的事件驱动服务器处理共享数据时不需要加锁?
查看参考答案
因为同一时刻只有一个处理函数在执行,不存在两条执行流同时读写同一份数据的可能。
继续查证
标准草案与官方章节会更新;版本标记只说明示例最低要求。