C++ / a working model

137 / 163   ·   C11   ·   约 8 分钟

基于事件的并发(进阶)

先记住这句话

本章介绍不依赖线程构建并发服务器的方法。程序围绕事件循环运行,一次只处理一个到达的事件,从而获得对调度的完全控制并消除锁的需求。关键限制是处理程序绝不能执行会阻塞的操作。

本篇内容
  1. 事件循环如何工作
  2. 用select监视多个描述符
  3. 单线程执行消除锁
  4. 阻塞调用会冻结整个循环
  5. 运行示例
  6. 动手练习

官方章节 PDF

事件循环如何工作

服务器进入一个永不退出的循环,每次迭代先收集当前待处理的事件,再依次调用对应的处理函数。处理函数只做少量工作便立即返回,循环因此始终掌握下一步该执行什么。开发者由此直接决定任务顺序,而不把决定权交给操作系统调度器。

用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上的事件驱动服务器处理共享数据时不需要加锁?

查看参考答案

因为同一时刻只有一个处理函数在执行,不存在两条执行流同时读写同一份数据的可能。

继续查证

标准草案与官方章节会更新;版本标记只说明示例最低要求。

回到目录