ae事件循环

ae 事件循环 源码分析(单线程 Reactor + IO 线程分担读写)

ae 是 Redis 自己写的极简事件库,解决的问题:一个线程如何同时伺候几万个客户端连接,还要顺带执行定时任务。
核心思路是 Reactor 模式——把「等 IO」交给内核多路复用(epoll),把「处理事件」留在单线程里顺序执行,等待超时精确设成距离最近定时器的时间,让一个 epoll_wait 同时兼顾网络事件和定时器。

// 基于本地 Redis 仓 (unstable), src/ae.h
#define AE_NONE 0       /* No events registered. */
#define AE_READABLE 1   /* Fire when descriptor is readable. */
#define AE_WRITABLE 2   /* Fire when descriptor is writable. */
#define AE_BARRIER 4    // 与 WRITABLE 联用:本轮已触发过读事件就先写后读反转顺序,用于"先 fsync 再回包"

/* File event structure */
typedef struct aeFileEvent {
    int mask; /* one of AE_(READABLE|WRITABLE|BARRIER) */
    aeFileProc *rfileProc;      // 读回调,Redis 里注册的是 readQueryFromClient
    aeFileProc *wfileProc;      // 写回调,如 sendReplyToClient
    void *clientData;           // 一般挂 connection/client 指针
} aeFileEvent;

/* Time event structure */
typedef struct aeTimeEvent {
    long long id; /* time event identifier. */
    monotime when;              // 绝对触发时刻(单调时钟微秒),不受系统调时间影响
    aeTimeProc *timeProc;       // Redis 里几乎只有一个:serverCron
    aeEventFinalizerProc *finalizerProc;
    void *clientData;
    struct aeTimeEvent *prev;   // 无序双向链表——Redis 定时器极少,O(N) 扫也无所谓
    struct aeTimeEvent *next;
    int refcount; /* refcount to prevent timer events from being
  		   * freed in recursive time event calls. */
} aeTimeEvent;

/* State of an event based program */
typedef struct aeEventLoop {
    int maxfd;   /* highest file descriptor currently registered */
    int setsize; /* max number of file descriptors tracked */
    long long timeEventNextId;
    int nevents; /* Size of Registered events */
    aeFileEvent *events; /* Registered events */    // 关键设计:fd 直接当数组下标,O(1) 找回调
    aeFiredEvent *fired; /* Fired events */         // epoll_wait 返回的就绪事件抄到这里
    aeTimeEvent *timeEventHead;
    int stop;
    void *apidata; /* This is used for polling API specific data */  // epoll 版里是 aeApiState
    aeBeforeSleepProc *beforesleep;     // 每轮睡前钩子,Redis 把大量收尾工作塞在这
    aeBeforeSleepProc *aftersleep;
    int flags;
    void *privdata[2];
} aeEventLoop;
// 基于本地 Redis 仓 (unstable), src/ae.c
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {          // 整个 Redis 主线程的一生就是这个 while
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|
                                   AE_CALL_BEFORE_SLEEP|
                                   AE_CALL_AFTER_SLEEP);
    }
}

int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData)
{
    if (fd >= eventLoop->setsize) {
        errno = ERANGE;
        return AE_ERR;
    }
    // ... fd 超过 nevents 时对 events/fired 数组翻倍扩容,略

    aeFileEvent *fe = &eventLoop->events[fd];   // fd 即下标,无需哈希表

    if (aeApiAddEvent(eventLoop, fd, mask) == -1)   // 落到 epoll_ctl
        return AE_ERR;
    fe->mask |= mask;                   // 或运算:同一 fd 可同时挂读+写
    if (mask & AE_READABLE) fe->rfileProc = proc;
    if (mask & AE_WRITABLE) fe->wfileProc = proc;
    fe->clientData = clientData;
    if (fd > eventLoop->maxfd)
        eventLoop->maxfd = fd;
    return AE_OK;
}

/* How many microseconds until the first timer should fire.
 * If there are no timers, -1 is returned. */
// ...
static int64_t usUntilEarliestTimer(aeEventLoop *eventLoop) {
    aeTimeEvent *te = eventLoop->timeEventHead;
    if (te == NULL) return -1;

    aeTimeEvent *earliest = NULL;
    while (te) {                        // O(N) 遍历找最早的定时器,N 极小所以不排序
        if ((!earliest || te->when < earliest->when) && te->id != AE_DELETED_EVENT_ID)
            earliest = te;
        te = te->next;
    }

    monotime now = getMonotonicUs();
    return (now >= earliest->when) ? 0 : earliest->when - now;  // 已过期返回 0 = 不睡
}

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;

    /* Nothing to do? return ASAP */
    if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;

    // 注意条件:即使一个 fd 都没有,只要有时间事件也要走 aeApiPoll——借它睡到定时器到期
    if (eventLoop->maxfd != -1 ||
        ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
        int j;
        struct timeval tv, *tvp = NULL; /* NULL means infinite wait. */
        int64_t usUntilTimer;

        if (eventLoop->beforesleep != NULL && (flags & AE_CALL_BEFORE_SLEEP))
            eventLoop->beforesleep(eventLoop);      // 睡前钩子:回写客户端、刷 AOF 等

        // ... 注释略:beforesleep 内可能改 eventLoop->flags,所以在其后再判断
        if ((flags & AE_DONT_WAIT) || (eventLoop->flags & AE_DONT_WAIT)) {
            tv.tv_sec = tv.tv_usec = 0;             // 还有活没干完,poll 一下立刻返回
            tvp = &tv;
        } else if (flags & AE_TIME_EVENTS) {
            usUntilTimer = usUntilEarliestTimer(eventLoop);   // 第一步:算最近定时器还有多久
            if (usUntilTimer >= 0) {
                tv.tv_sec = usUntilTimer / 1000000;
                tv.tv_usec = usUntilTimer % 1000000;
                tvp = &tv;              // 把它作为 epoll_wait 的超时——定时器不需要独立线程
            }
        }
        /* Call the multiplexing API, will return only on timeout or when
         * some event fires. */
        numevents = aeApiPoll(eventLoop, tvp);      // 第二步:阻塞在这里,唯一的"睡觉"点

        /* Don't process file events if not requested. */
        if (!(flags & AE_FILE_EVENTS)) {
            numevents = 0;
        }

        /* After sleep callback. */
        if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)
            eventLoop->aftersleep(eventLoop);

        for (j = 0; j < numevents; j++) {           // 第三步:先处理文件事件
            int fd = eventLoop->fired[j].fd;
            aeFileEvent *fe = &eventLoop->events[fd];
            int mask = eventLoop->fired[j].mask;
            int fired = 0; /* Number of events fired for current fd. */

            // ... 注释略:常规顺序是先读后写——读完命令马上能把回复写出去,省一轮循环
            int invert = fe->mask & AE_BARRIER;     // BARRIER 反转成先写后读

            // fe->mask & mask 再查一次:前面的回调可能已经把这个事件删了
            if (!invert && fe->mask & mask & AE_READABLE) {
                fe->rfileProc(eventLoop,fd,fe->clientData,mask);
                fired++;
                fe = &eventLoop->events[fd]; /* Refresh in case of resize. */   // 回调里可能 realloc 了 events 数组
            }

            /* Fire the writable event. */
            if (fe->mask & mask & AE_WRITABLE) {
                if (!fired || fe->wfileProc != fe->rfileProc) { // 读写回调是同一个函数时不重复调
                    fe->wfileProc(eventLoop,fd,fe->clientData,mask);
                    fired++;
                }
            }

            /* If we have to invert the call, fire the readable event now
             * after the writable one. */
            if (invert) {
                fe = &eventLoop->events[fd]; /* Refresh in case of resize. */
                if ((fe->mask & mask & AE_READABLE) &&
                    (!fired || fe->wfileProc != fe->rfileProc))
                {
                    fe->rfileProc(eventLoop,fd,fe->clientData,mask);
                    fired++;
                }
            }

            processed++;
        }
    }
    /* Check time events */
    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);  // 第四步:最后才处理时间事件

    return processed; /* return the number of processed file/time events */
}
// 基于本地 Redis 仓 (unstable), src/ae_epoll.c —— Linux 下编译期选中的多路复用实现
typedef struct aeApiState {
    int epfd;                           // epoll 实例的 fd
    struct epoll_event *events;         // epoll_wait 的输出缓冲区
} aeApiState;

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee = {0}; /* avoid valgrind warning */
    /* If the fd was already monitored for some event, we need a MOD
     * operation. Otherwise we need an ADD operation. */
    int op = eventLoop->events[fd].mask == AE_NONE ?
            EPOLL_CTL_ADD : EPOLL_CTL_MOD;      // 靠 ae 自己记的 mask 区分 ADD/MOD,不用试错

    ee.events = 0;
    mask |= eventLoop->events[fd].mask; /* Merge old events */   // 新旧事件合并,MOD 是全量覆盖语义
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.fd = fd;
    if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
    return 0;
}

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;

    retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
            tvp ? (tvp->tv_sec*1000 + (tvp->tv_usec + 999)/1000) : -1);   // 微秒向上取整成毫秒;tvp==NULL 则无限等
    if (retval > 0) {
        int j;

        numevents = retval;
        for (j = 0; j < numevents; j++) {
            int mask = 0;
            struct epoll_event *e = state->events+j;

            if (e->events & EPOLLIN) mask |= AE_READABLE;
            if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
            if (e->events & EPOLLERR) mask |= AE_WRITABLE|AE_READABLE;  // 出错转成读写事件,让回调里的 read/write 拿到错误码
            if (e->events & EPOLLHUP) mask |= AE_WRITABLE|AE_READABLE;
            eventLoop->fired[j].fd = e->data.fd;    // 翻译成平台无关的 fired 数组
            eventLoop->fired[j].mask = mask;
        }
    } else if (retval == -1 && errno != EINTR) {
        panic("aeApiPoll: epoll_wait, %s", strerror(errno));
    }

    return numevents;
}

原理串讲

一次完整的循环:main 里调 aeMain,死循环调 aeProcessEvents
每轮先执行 beforesleep 钩子(Redis 注册的是 server.c 的 beforeSleep),然后 usUntilEarliestTimer 遍历定时器链表算出最近一个定时器还有多少微秒到期,把这个值填进 timeval 传给 aeApiPoll,最终成为 epoll_wait 的超时参数。
于是线程要么被网络事件唤醒,要么恰好睡到定时器该触发的时刻醒来——这是第一个「为什么」:定时器不需要额外线程或信号,把”最近的闹钟”折算成 epoll 超时,一个阻塞点就同时管住了 IO 和时间,代价只是定时器精度受限于本轮循环长度,所以 serverCron 的设计也是”到点了做一小片活”而不是精确调度。
醒来后先遍历 fired 数组处理文件事件:从 events[fd] 取出回调,默认先 rfileProcwfileProc——先读是因为读完命令、执行完,回复往往已经躺在输出缓冲区里,紧接着的写事件就能立刻把它发出去;文件事件全部处理完才轮到 processTimeEvents 扫链表执行到期的 serverCron

beforeSleep(server.c)是理解 Redis 的关键钩子,它承包了”命令执行完到真正入睡之间”的所有收尾:快速过期扫描 activeExpireCycle(ACTIVE_EXPIRE_CYCLE_FAST)、唤醒解除阻塞的客户端 blockedBeforeSleep、把 AOF 缓冲刷盘 flushAppendOnlyFile,以及最重要的 handleClientsWithPendingWrites——把本轮所有命令产生的回复统一回写。
这是第二个「为什么」:命令执行时只把回复追加到 client 的输出缓冲区并把 client 挂进 pending 队列,不立即 write
这样同一个客户端在一轮里发的多条流水线命令,回复会攒起来一次 write 系统调用发走,syscall 次数从 N 降到 1;只有一次没写完的,才注册 AE_WRITABLE 事件交给下一轮。
AE_BARRIER 服务于同一个钩子:appendfsync=always 时必须保证 beforeSleep 里先 fsync 再回包,所以要禁止”读到命令后顺手把上一轮回复写出去”的默认顺序。

代码块JAVA · 2 行收起展开
单线程为什么快:没有锁、没有上下文切换、没有并发数据结构,所有对键空间的访问天然串行,这让 dict/ziplist 这些结构可以做得极其紧凑;而瓶颈本来就不在 CPU 计算——大多数命令是 O(1)/O(logN) 的内存操作,真正的开销在网络 IO 的 read/write 和协议解析上。
所以 unstable 版的多线程方案(src/iothread.c)也只动 IO 不动执行:`initThreadedIO` 为每个 IO 线程各建一个独立的 `aeEventLoop`,`IOThreadMain` 里同样跑 `aeMain`,客户端连接被 `assignClientToIOThread` 绑定到某个 IO 线程后,读事件、协议解析、回复写出都在那个线程的事件循环里完成;但一旦解析出完整命令,client 会被挂进带互斥锁的 `mainThreadPendingClients` 链表并通过 eventNotifier 唤醒主线程,主线程在 `processClientsFromIOThread` 里逐个取出、置 `running_tid` 为主线程后调 `processPendingCommandAndInputBuffer` 执行命令,执行完再把 client 交还 IO 线程去写回复。

命令执行始终单线程,键空间依旧无锁,多线程只是把最贵的 read/write/解析摊出去——两边通过”同一时刻一个 client 只属于一个线程”来避免加锁访问 client 数据。

设计取舍

  • 定时器用无序链表,找最近定时器 O(N):源码注释自己承认可以换跳表,但 Redis 定时器就一个 serverCron 量级,不值得。
  • events 数组用 fd 直接当下标:O(1) 且无哈希开销,代价是数组大小要按 maxclients 预留,fd 稀疏时浪费内存——服务器场景 fd 密集,划算。
  • 先读后写不是随便定的:读→执行→写在同一轮完成,省掉一整轮事件循环的延迟;AE_BARRIER 是这个优化的逃生门,不是常态。
  • 常见误区:「Redis 6 之后是多线程了」——IO 线程只做 read/解析/write,命令执行和键空间访问从来都在主线程,事务和 Lua 的原子性保证不变。
  • epoll_wait 超时向上取整((tv_usec + 999)/1000):宁可多睡不提前醒,提前醒会发现定时器没到期又空转一圈。

延伸阅读