《redis设计与实现》-12事件event

一 序

    书上主要介绍了文件事件、时间事件,事件调度三部分,结合源码来看,主要分在ae.hae.c 与networking.c两大块。限于篇幅。本文分两部分,第一部分整理书上概念,第二部分看一下时间事件及文件事件的部分源码。文件事件的处理器部分单独整理。

二 事件类型

Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件:

  • 文件事件(file event):Redis服务器通过套接字与客户端(或者其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或者其他服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作;
  • 时间事件(time event):Redis服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。

      所谓事件驱动,就是当你输入一条命令并且按下回车,然后消息被组装成Redis协议的格式发送给Redis服务器,这就会产生一个事件,Redis服务器会接收该命令,处理该命令和发送回复,而当你没有与服务器进行交互时,那么服务器就会处于阻塞等待状态,会让出CPU从而进入睡眠状态,当事件触发时,就会被操作系统唤醒。事件驱动使CPU更高效的利用。也可以称为I/O多路复用(multiplexing)。

  文件事件的结构,源码在ae.h

/* File event structure */
typedef struct aeFileEvent {
    // 文件时间类型:AE_NONE,AE_READABLE,AE_WRITABLE
    int mask; /* one of AE_(READABLE|WRITABLE) */
    // 可读处理函数
    aeFileProc *rfileProc;
    // 可写处理函数
    aeFileProc *wfileProc;
    // 客户端传入的数据
    void *clientData;
} aeFileEvent;  //文件事件

其中rfileProcwfileProc成员分别为两个函数指针 ,这个函数是回调函数,如果当前文件事件所指定的事件类型发生时,则会调用对应的回调函数处理该事件.

当事件就绪时,我们需要知道文件事件的文件描述符还有事件类型才能对于锁定该事件,因此定义了aeFiredEvent结构统一管理:源码在ae.h

/* A fired event 就绪事件*/
typedef struct aeFiredEvent {
    int fd;//就绪事件的文件描述符
    int mask;//就绪事件类型:AE_NONE,AE_READABLE,AE_WRITABLE
} aeFiredEvent;

 时间事件的结构:


/* Time event structure 时间事件结构*/
typedef struct aeTimeEvent {
	  //时间时间的ID
    long long id; /* time event identifier. */
    // 时间事件到达的时间的秒数
    long when_sec; /* seconds */
    // 时间事件到达的时间的毫秒数
    long when_ms; /* milliseconds */
    //时间事件处理函数
    aeTimeProc *timeProc;
    // 时间事件终结函数
    aeEventFinalizerProc *finalizerProc;
    // 时间事件终结函数
    void *clientData;
     // 指向下一个时间事件
    struct aeTimeEvent *next;
} aeTimeEvent;

从这个结构中可以看出,时间事件表是一个链表,因为它有一个next指针域,指向下一个时间事件。和文件事件一样,当时间事件所指定的事件发生时,也会调用对应的回调函数,结构成员timeProcfinalizerProc都是回调函数。 

三 文件事件

  3.1文件事件处理器

  • 文件事件处理器使用I/O多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器;
  • 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件

    虽然文件事件处理器以单线程运行,但是通过使用I/O多路复用(multiplexing)程序来同时监听多个套接字,文件事件处理器即实现了高性能的网络通信模型,也可以很好的与redis服务器中其它以单线程运行的模块进行对接,这保持了redis内部单线程设计的简单性。

                       文件事件处理器的四个组成部分

文件事件是对套接字操作的抽象,每当一个套接字准备好执行连接应答(accept)、写入、读取、关闭等操作时,就会产生一个文件事件。因为一个服务器通常会连接多个套接字,所以多个文件事件有可能会并发地出现。

I/O多路复用程序负责监听多个套接字,并向文件事件分派器传送那些产生了事件的套接字。

尽管多个文件事件可能会并发地出现,但I/O多路复用程序总是会将所有产生事件的套接字都放到一个队列里面,然后通过这个队列,以有序(sequentially)、同步(synchronously)、每次一个套接字的方式向文件事件分派器传送套接字。

文件事件分派器接收I/O多路复用程序传来的套接字,并根据套接字产生的事件的类型,调用相应的事件处理器;

事件处理器是一个个函数,它们定义了某个事件发生时,服务器应该执行的动作。

I/O多路复用程序的实现

Redis的I/O多路复用程序的所有功能都是通过包装常见的select、epoll、evport和kqueue这些I/O多路复用函数库来实现的,每个I/O多路复用函数库在Redis源码中都对应一个单独的文件,比如ae_select.c、ae_epoll.c、ae_kqueue.c,诸如此类。因为Redis为每个I/O多路复用函数库都实现了相同的API,所以I/O多路复用程序的底层实现是可以互换,如图:

事件状态结构

/* State of an event based program */
//事件处理器的状态
typedef struct aeEventLoop {
	  // 目前已注册的最大描述符
    int maxfd;   /* highest file descriptor currently registered */
    // 文件描述符监听集合的大小
    int setsize; /* max number of file descriptors tracked */
     // 用于生成时间事件 id(下一个时间事件的ID)
    long long timeEventNextId;
    // 最后一次执行事件的时间
    time_t lastTime;     /* Used to detect system clock skew */
    // 注册的文件事件表
    aeFileEvent *events; /* Registered events */
     // 已就绪的文件事件表
    aeFiredEvent *fired; /* Fired events */
    //时间事件
    aeTimeEvent *timeEventHead;
    // 事件处理器的开关
    int stop;
    // 多路复用库的事件状态数据
    void *apidata; /* This is used for polling API specific data */
    // 执行处理事件之前的函数
    aeBeforeSleepProc *beforesleep;
} aeEventLoop;

aeEventLoop结构保存了一个void *类型的万能指针apidata,是用来保存轮询事件的状态的,也就是保存底层调用的多路复用库的事件状态,上面说了Redis多路复用程序底层实现是可以互换的。具体选择库的源码为ae.c:


/* Include the best multiplexing layer supported by this system.
 * The following should be ordered by performances, descending. */
 // IO复用的选择,性能依次下降,Linux支持 "ae_epoll.c" 和 "ae_select.c"
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
    #ifdef HAVE_EPOLL
    #include "ae_epoll.c"
    #else
        #ifdef HAVE_KQUEUE
        #include "ae_kqueue.c"
        #else
        #include "ae_select.c"
        #endif
    #endif
#endif

3.2事件的类型

I/O多路复用程序可以监听多个套接字的ae.h/AE_READABLE事件和ae.h/AE_WRITABLE事件,这两类事件和套接字操作之间的对应关系如下:

  • 当套接字变得可读时(客户端对套接字执行write操作,或者执行close操作),或者有新的可应答(acceptable)套接字出现时(客户端对服务器的监听套接字执行connect操作),套接字产生AE_READABLE事件;
  • 当套接字变得可写时(客户端对套接字执行read操作),套接字产生AE_WRITABLE事件。

I/O多路复用程序允许服务器同时监听套接字的AE_READABLE事件和AE_WRITABLE事件,如果一个套接字同时产生了这两种事件,那么文件事件分派器会优先处理AE_READABLE事件,等到AE_READABLE事件处理完之后,才处理AE_WRITABLE事件。

3.3 API

  就是ae.c的实现

// 创建一个epoll实例,保存到eventLoop中
static int aeApiCreate(aeEventLoop *eventLoop)
// 调整事件表的大小
static int aeApiResize(aeEventLoop *eventLoop, int setsize)  
// 释放epoll实例和事件表空间
static void aeApiFree(aeEventLoop *eventLoop)
// 在epfd标识的事件表上注册fd的事件
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask)
// 在epfd标识的事件表上注删除fd的事件
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask)
// 等待所监听文件描述符上有事件发生
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)
// 返回正在使用的IO多路复用库的名字
static char *aeApiName(void)

3.4文件事件的处理器

连接应答处理器

networking.c/acceptTcpHandler函数是Redis的连接应答处理器,这个处理器用于对连接服务器监听套接字的客户端进行应答,其主要调用anet.c中anetTcpAccept函数实现,具体实现为sys/socket.h/accept函数的包装。

当Redis服务器进行初始化的时候,程序会将这个连接应答处理器和服务器监听套接字的AE_READABLE事件关联起来,当有客户端用sys/socket.h/connect函数连接服务器监听套接字的时候,套接字就会产生AE_READABLE事件,引发连接应答处理器执行,并执行相应的套接字应答操作。

命令请求处理器

networking.c/readQueryFromClient函数是Redis的命令请求处理器,这个处理器负责从套接字中读入客户端发送的命令请求内容,具体实现为unistd.h/read函数的包装。

当一个客户端通过连接应答处理器成功连接到服务器之后,服务器会将客户端套接字的AE_READABLE事件和命令请求处理器关联起来,当客户端向服务器发送命令请求的时候,套接字就会产生AE_READABLE事件,引发命令请求处理器执行,并执行相应的套接字读入操作;

在客户端连接服务器的整个过程中,服务器都会一直为客户端套接字的AE_READABLE事件关联命令请求处理器。

命令回复处理器

networking.c/sendReplyToClient函数是Redis的命令回复处理器,这个处理器负责将服务器执行命令后得到的命令回复通过套接字返回给客户端,具体实现为unistd.h/write函数的包装。

服务器有命令回复需要传送给客户端的时候服务器会将客户端套接字的AE_WRITABLE事件和命令回复处理器关联起来,当客户端准备好接收服务器传回的命令回复时,就会产生AE_WRITABLE事件,引发命令回复处理器执行,并执行相应的套接字写入操作

  本篇不展开此处相关的源码。

四 时间事件

 时间事件分为以下两类:

  • 定时事件:让一段程序在指定的时间之后执行一次。比如说,让程序X在当前时间的30毫秒之后执行一次;
  • 周期性事件:让一段程序每隔指定时间就执行一次。比如说,让程序Y每隔30毫秒就执行一次。

时间事件主要由以下三个属性组成:

  • id:服务器为时间事件创建的全局唯一ID(标识号)。ID号按从小到大的顺序递增,新事件的ID号比旧事件的ID号要大;
  • when:毫秒精度的UNIX时间戳,记录了时间事件的到达(arrive)时间;
  • timeProc:时间事件处理器,一个函数。当时间事件到达时,服务器就会调用相应的处理器来处理事件。

一个时间事件是定时事件还是周期性事件取决于时间事件处理器的返回值:

  • 如果事件处理器返回ae.h/AE_NOMORE,那么这个事件为定时事件:该事件在达到一次之后就会被删除,之后不再到达;
  • 如果事件处理器返回一个非AE_NOMORE的整数值,那么这个事件为周期性时间:当一个时间事件到达之后,服务器会根据事件处理器返回的值,对时间事件的when属性进行更新,让这个事件在一段时间之后再次到达,并以这种方式一直更新并运行下去。比如说,如果一个时间事件的处理器返回整数值30,那么服务器应该对这个时间事件进行更新,让这个事件在30毫秒之后再次到达。(现在的Redis主要使用这个)

4.1API

ae.c/aeCreateTimeEvent函数接受一个毫秒数milliseconds和一个时间事件处理器proc作为参数,将一个新的时间事件添加到服务器,这个新的时间事件将在当前时间的milliseconds毫秒之后到达,而事件的处理器为proc。

ae.c/aeDeleteFileEvent函数接受一个时间事件ID作为参数,然后从服务器中删除该ID所对应的时间事件;

ae.c/aeSearchNearestTimer函数返回到达时间距离当前时间最接近的那个时间事件;

ae.c/processTimeEvents函数是时间事件的执行器,这个函数会遍历所有已到达的时间事件,并调用这些事件的处理器。已到达指的是,时间事件的when属性记录的UNIX时间戳等于或小于当前时间的UNIX时间戳。

processTimeEvents 源码如下:

/* Process time events 处理所有已到达的时间事件*/
static int processTimeEvents(aeEventLoop *eventLoop) {
    int processed = 0;
    aeTimeEvent *te, *prev;
    long long maxId;
    time_t now = time(NULL);

    /* If the system clock is moved to the future, and then set back to the
     * right value, time events may be delayed in a random way. Often this
     * means that scheduled operations will not be performed soon enough.
     *
     * Here we try to detect system clock skews, and force all the time
     * events to be processed ASAP when this happens: the idea is that
     * processing events earlier is less dangerous than delaying them
     * indefinitely, and practice suggests it is. */
     // 通过重置事件的运行时间,
    // 防止因时间穿插(skew)而造成的事件处理混乱 (上一次处理事件的时间比当前时间还要大)
    if (now < eventLoop->lastTime) {
        te = eventLoop->timeEventHead;
        while(te) {
            te->when_sec = 0;
            te = te->next;
        }
    }
    // 更新最后一次处理时间事件的时间为当前时间
    eventLoop->lastTime = now;
    
    
    /* 遍历链表
    * 执行那些已经到达的事件*/
    prev = NULL;
    te = eventLoop->timeEventHead;
    maxId = eventLoop->timeEventNextId-1;//当前时间事件表中的最大ID
     // 遍历时间事件链表
    while(te) {
        long now_sec, now_ms;
        long long id;

        /* Remove events scheduled for deletion. */
         // 如果时间事件已被删除了
        if (te->id == AE_DELETED_EVENT_ID) {
            aeTimeEvent *next = te->next;
            // 从事件链表中删除事件的节点
            if (prev == NULL)
                eventLoop->timeEventHead = te->next;
            else
                prev->next = te->next;
             // 调用时间事件终结方法清除该事件    
            if (te->finalizerProc)
                te->finalizerProc(eventLoop, te->clientData);
            zfree(te);
            te = next;
            continue;
        }

        /* Make sure we don't process time events created by time events in
         * this iteration. Note that this check is currently useless: we always
         * add new timers on the head, however if we change the implementation
         * detail, this check may be useful again: we keep it here for future
         * defense. */
         // 跳过无效事件 
        if (te->id > maxId) {
            te = te->next;
            continue;
        }
         // 获取当前时间
        aeGetTime(&now_sec, &now_ms);
          // 如果当前时间等于或等于事件的执行时间,那么说明事件已到达,执行这个事件
        if (now_sec > te->when_sec ||
            (now_sec == te->when_sec && now_ms >= te->when_ms))
        {
            int retval;

            id = te->id;
             // 执行事件处理器,并获取返回值
            retval = te->timeProc(eventLoop, id, te->clientData);
             // 时间事件次数加1
            processed++;
             // 如果不是定时事件,则继续设置它的到时时间
            if (retval != AE_NOMORE) {
                aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms);
            } else {// 将这个事件删除
                te->id = AE_DELETED_EVENT_ID;
            }
        }
        // 因为执行事件之后,事件列表可能已经被改变了
        // 更新前驱节点指针和后继节点指针
        prev = te;
        te = te->next;
    }
    return processed;//返回执行事件的次数
}

 4.2 时间事件应用实例:serverCron函数

持续运行的Redis服务器需要定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行,这些定期操作由redis.c/serverCron函数负责执行,它的主要工作包括:

  • 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等;
  • 清理数据库中的过期键值对;
  • 关闭和清理连接失效的客户端;
  • 尝试进行AOF或RDB持久化操作;
  • 如果服务器是主服务器,那么对从服务器进行定期同步;
  • 如果处于集群模式,对集群进行定期同步和连接测试;

五 事件的调度与执行

Redis事件的源码全部定义在ae.c文件中,我们从事件的主函数aeMain说起,

//事件轮询的主函数
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    //不停止一直在处理
    while (!eventLoop->stop) {
    	  // 执行处理事件之前的函数
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        //处理到时的时间事件和就绪的文件事件
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

如果服务器一直处理事件,那么就是一个死循环,而一个最典型的事件驱动,就是一个死循环。调用处理事件的函数aeProcessEvents,他们参数是一个事件状态结构aeEventLoopAE_ALL_EVENTS,源码如下:

/* Process every pending time event, then every pending file event
 * (that may be registered by time event callbacks just processed).
 * Without special flags the function sleeps until some file event
 * fires, or when the next time event occurs (if any).
 * 处理到时的时间事件和就绪的文件事件
 * If flags is 0, the function does nothing and returns.
 * if flags has AE_ALL_EVENTS set, all the kind of events are processed.
 * if flags has AE_FILE_EVENTS set, file events are processed.
 * if flags has AE_TIME_EVENTS set, time events are processed.
 * if flags has AE_DONT_WAIT set the function returns ASAP until all
 * the events that's possible to process without to wait are processed.
 *
 * The function returns the number of events processed.
 *函数的返回值为已处理事件的数量
  */
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;

    /* Note that we want call select() even if there are no
     * file events to process as long as we want to process time
     * events, in order to sleep until the next time event is ready
     * to fire.
     *既然我们要处理时间事件,即使没有要处理的文件事件,
     *我们仍要调用select(),以便在下一次事件准备启动之前进行休眠
     *
      */
      // 当前还没有要处理的文件事件,或者设置了时间事件但是没有设置不阻塞标识
    if (eventLoop->maxfd != -1 ||
        ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
        int j;
        aeTimeEvent *shortest = NULL;
        struct timeval tv, *tvp;
         
         //设置了时间事件但是没有设置不阻塞标识
        if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
        	 // 获取最近到时的时间事件
            shortest = aeSearchNearestTimer(eventLoop);
        if (shortest) {
        	  // 如果时间事件存在的话
            // 那么根据最近可执行时间事件和现在时间的时间差来决定文件事件的阻塞时间
            long now_sec, now_ms;
            // 获取当前时间
            aeGetTime(&now_sec, &now_ms);
            tvp = &tv;

            /* How many milliseconds we need to wait for the next
             * time event to fire? */
             // 等待该时间事件到时所需要的时长 
            long long ms =
                (shortest->when_sec - now_sec)*1000 +
                shortest->when_ms - now_ms;
            
            // 如果没到时
            if (ms > 0) {// 保存时长到tvp中
                tvp->tv_sec = ms/1000;
                tvp->tv_usec = (ms % 1000)*1000;
            } else {  // 如果已经到时可以执行了,则将tvp的时间设置为0
                tvp->tv_sec = 0;
                tvp->tv_usec = 0;
            }
        } else { // 没有获取到了最早到时的时间事件,时间事件链表为空
        	       //那么根据 AE_DONT_WAIT 是否设置来决定是否阻塞,以及阻塞的时间长度
            /* If we have to check for events but need to return
             * ASAP because of AE_DONT_WAIT we need to set the timeout
             * to zero */
              // 如果设置了不阻塞标识 
            if (flags & AE_DONT_WAIT) {
            	  // 将tvp的时间设置为0,就不会阻塞
                tv.tv_sec = tv.tv_usec = 0;
                tvp = &tv;
            } else {
                /* Otherwise we can block */
                // 阻塞到有时间事件的到来
                tvp = NULL; /* wait forever */
            }
        }
         // 等待所监听文件描述符上有事件发生
        // 处理文件事件,阻塞时间由 tvp 决定(如果tvp为NULL,则阻塞在此,否则等待tvp设置阻塞的时间,就会有时间事件到时返回了就绪文件事件的个数)
        numevents = aeApiPoll(eventLoop, tvp);
         // 遍历就绪文件事件表
        for (j = 0; j < numevents; j++) {
        	   // 从已就绪数组中获取事件
            aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
             // 获取就绪文件事件的类型,文件描述符
            int mask = eventLoop->fired[j].mask;
            int fd = eventLoop->fired[j].fd;
            int rfired = 0;

	    /* note the fe->mask & mask & ... code: maybe an already processed
             * event removed an element that fired and we still didn't
             * processed, so we check if the event is still valid. */
            // 如果是文件可读事件发生 
            if (fe->mask & mask & AE_READABLE) {
            	   // 设置读事件标识(确保读/写事件只能执行其中一个)
                rfired = 1;
                // 调用读事件方法处理读事件
                fe->rfileProc(eventLoop,fd,fe->clientData,mask);
            } 
            // 如果是文件可写事件发生
            if (fe->mask & mask & AE_WRITABLE) {
            	  // 读写事件的执行发法不同,则执行写事件,避免重复执行相同的方法
                if (!rfired || fe->wfileProc != fe->rfileProc)
                    fe->wfileProc(eventLoop,fd,fe->clientData,mask);
            }
            processed++;//执行的事件次数加1
        }
    }
    /* Check time events */
    // 执行时间事件
    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);

    return processed; /* return the number of processed file/time events */
}

刚才提到该函数的一个参数是AE_ALL_EVENTS,他的定义在ae.h中,定义如下:

#define AE_FILE_EVENTS 1                                //文件事件
#define AE_TIME_EVENTS 2                                //时间事件
#define AE_ALL_EVENTS (AE_FILE_EVENTS|AE_TIME_EVENTS)   //文件和时间事件
#define AE_DONT_WAIT 4                                  //不阻塞等待标识

很明显,flags是AE_FILE_EVENTS和AE_TIME_EVENTS或的结果,他们的含义如下:

如果flags = 0,函数什么都不做,直接返回
如果flags设置了 AE_ALL_EVENTS ,则执行所有类型的事件
如果flags设置了 AE_FILE_EVENTS ,则执行文件事件
如果flags设置了 AE_TIME_EVENTS ,则执行时间事件
如果flags设置了 AE_DONT_WAIT ,那么函数处理完事件后直接返回,不阻塞等待

Redis服务器在没有被事件触发时,就会阻塞等待,因为没有设置AE_DONT_WAIT标识。但是他不会一直的死等待,等待文件事件的到来,因为他还要处理时间时间,因此,在调用aeApiPoll进行监听之前,先从时间事件表中获取一个最近到达的时间时间,根据要等待的时间构建一个struct timeval tv, *tvp结构的变量,这个变量保存着服务器阻塞等待文件事件的最长时间,一旦时间到达而没有触发文件事件,aeApiPoll函数就会停止阻塞,进而调用processTimeEvents处理时间事件,因为Redis服务器设定一个对自身资源和状态进行检查的周期性检查的时间事件,而该函数就是timeProc所指向的回调函数serverCron().

如果在阻塞等待的最长时间之间,触发了文件事件,就会先执行文件事件,后执行时间事件,因此处理时间事件通常比预设的会晚一点。

六其他部分源码:

//创建时间事件
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
        aeTimeProc *proc, void *clientData,
        aeEventFinalizerProc *finalizerProc)
{
	  //更新时间计数器
    long long id = eventLoop->timeEventNextId++;
    // 创建时间事件结构
    aeTimeEvent *te;

    te = zmalloc(sizeof(*te));
    if (te == NULL) return AE_ERR;
    // 设置 ID	
    te->id = id;
     // 设定处理事件的时间
    aeAddMillisecondsToNow(milliseconds,&te->when_sec,&te->when_ms);
     // 设置事件处理器
    te->timeProc = proc;
    te->finalizerProc = finalizerProc;
    // 设置私有数据
    te->clientData = clientData;
    // 将新事件放入表头
    te->next = eventLoop->timeEventHead;
    eventLoop->timeEventHead = te;
    return id;
}

 

todo..

参考:

https://blog.csdn.net/men_wen/article/details/71514524

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值