libevent源码剖析

Deadly 提交于 2020-03-14 14:33:40

安装与使用

  libevent安装:下载地址:http://libevent.org/

  1. 解压文件:tar -zxvf libevent-2.1.8-stable.tar.gz
  2. 解压后进入目录,进行配置,把库安装到/usr目录下:./configure --prefix=/usr
  3. 编译安装:sudo make,sudo make install

  libevent将I/O事件、信号事件、定时事件三种事件进行了同一事件源,将所有的就绪事件,放入到激活链表中;然后对激活链表中的事件,调用事件的回调函数执行事件处理

一、event_base

  The event_base lies at the center of Libevent; every application will have one.对应的为Reactor实例,使用 libevent 函数之前需要分配一个或者多个 event_base 结构体。每个event_base
结构体持有一个事件集合,可以检测以 确定哪个事件是激活的(相当于epoll红黑树的树根)。

  因为不是所有的安插在event_base的事件在调用fork()之后都可以正常工作,所以,如果在使用fork()或者其他相关系统调用启动一个新的进程之后,要想在子进程中使用base变量,但是又想让该base变量是一个全新的没有安插事件的变量,就应该在子进程中对base调用event_reinit函数进行重新初始化。

struct event_base {
    const struct eventop *evsel;
    void *evbase;
    int event_count; /* counts number of total events */
    int event_count_active; /* counts number of active events */
    int event_gotterm; /* Set to terminate loop */
    int event_break; /* Set to terminate loop immediately */
    /* active event management */
    struct event_list **activequeues;
    int nactivequeues;
    /* signal handling info */
    struct evsignal_info sig;
    struct event_list eventqueue;
    struct timeval event_tv;
    struct min_heap timeheap;
    struct timeval tv_cache;
};
struct eventop {
    const char *name;
    void *(*init)(struct event_base *); // 初始化
    int (*add)(void *, struct event *); // 注册事件
    int (*del)(void *, struct event *); // 删除事件
    19
    int (*dispatch)(struct event_base *, void *, struct timeval *); // 事件分发
    void (*dealloc)(struct event_base *, void *); // 注销,释放资源
    /* set if we need to reinitialize the event base */
    int need_reinit;
};
  1. libevent将系统提供的I/O demultiplex机制统一封装成了eventop结构,因此evsel指向全局变量中的一个
  2. evbase实际上是一个eventop实例对象
  3. activequeues是一个二级指针,前面讲过libevent支持事件优先级,因此你可以把它看作是数组,其中的元素activequeues[priority]是一个链表,链表的每个节点指向一个优先级为priority的就绪事件event。
  4. eventqueue,链表,保存了所有的注册事件event的指针。
  5. sig是由来管理信号的结构体
  6. timeheap是管理定时事件的小根堆
  7. event_tv和tv_cache是libevent用于时间管理的变量

初始化和创建event_base

  创建一个event_base对象也既是创建了一个新的libevent实例,程序需要通过调用event_init()(内部调用event_base_new函数执行具体操作)函数来创建,该函数同时还对新生成的libevent实例进行了初始化。

  该函数首先为event_base实例申请空间,然后初始化timer mini-heap,选择并初始化合适的系统I/O 的demultiplexer机制,初始化各事件链表;

注册事件

  向 Reactor(对应源码中的 event_base ) 中添加I/O、定时事件,需要调用 event_add 函数,信号事件调用 evsignal_add evsignal_add 最终也会调用 event_add

int event_add(struct event *ev, const struct timeval *tv);

int event_add(struct event *ev, const struct timeval *tv)
{
    struct event_base *base = ev->ev_base; // 要注册到的event_base
    const struct eventop *evsel = base->evsel;
    void *evbase = base->evbase; // base使用的系统I/O策略

    // 新的timer事件,调用timer heap接口在堆上预留一个位置
    // 注:这样能保证该操作的原子性:
    // 向系统I/O机制注册可能会失败,而当在堆上预留成功后,
    // 定时事件的添加将肯定不会失败;
    // 而预留位置的可能结果是堆扩充,但是内部元素并不会改变
    if (tv != NULL && !(ev->ev_flags & EVLIST_TIMEOUT)) {
        if (min_heap_reserve(&base->timeheap,1 + min_heap_size(&base->timeheap)) == -1)
            return (-1); /* ENOMEM == errno */
    }

    // 如果事件ev不在已注册或者激活链表中,则调用evbase注册事件
    if ((ev->ev_events & (EV_READ|EV_WRITE|EV_SIGNAL)) &&
        !(ev->ev_flags & (EVLIST_INSERTED|EVLIST_ACTIVE))) {
        res = evsel->add(evbase, ev);
        if (res != -1) // 注册成功,插入event到已注册链表中
            event_queue_insert(base, ev, EVLIST_INSERTED);
    }

    // 准备添加定时事件
    if (res != -1 && tv != NULL) {
        struct timeval now;
        // EVLIST_TIMEOUT表明event已经在定时器堆中了,删除旧的
        if (ev->ev_flags & EVLIST_TIMEOUT)
            event_queue_remove(base, ev, EVLIST_TIMEOUT);

        // 如果事件已经是就绪状态则从激活链表中删除
        if ((ev->ev_flags & EVLIST_ACTIVE) &&(ev->ev_res & EV_TIMEOUT)) {
            // 将ev_callback调用次数设置为0
            if (ev->ev_ncalls && ev->ev_pncalls) {
                *ev->ev_pncalls = 0;
            }
            event_queue_remove(base, ev, EVLIST_ACTIVE);
        }

        // 计算时间,并插入到timer小根堆中
        gettime(base, &now);
        evutil_timeradd(&now, tv, &ev->ev_timeout);
        event_queue_insert(base, ev, EVLIST_TIMEOUT);
    }
    return (res);
}
  1. ev:指向要注册的事件;
  2. tv:超时时间;

  函数将ev注册到ev->ev_base上,事件类型由ev->ev_events指明,如果注册成功,ev将被插入到已注册链表中;如果tv不是NULL,则会同时注册定时事件,将ev添加到timer堆上;如果其中有一步操作失败,那么函数保证没有事件会被注册,可以讲这相当于一个原子操作。

  event_queue_insert()负责将事件插入到对应的链表中

void event_queue_insert(struct event_base *base, struct event *ev, int queue)
{
    // ev可能已经在激活列表中了,避免重复插入
    if (ev->ev_flags & queue) {
        if (queue & EVLIST_ACTIVE)
        return;
    }
    
    // ...
    ev->ev_flags |= queue; // 记录queue标记
    switch (queue) {
        case EVLIST_INSERTED: // I/O或Signal事件,加入已注册事件链表
            TAILQ_INSERT_TAIL(&base->eventqueue, ev, ev_next);
            break;
        case EVLIST_ACTIVE: // 就绪事件,加入激活链表
            base->event_count_active++;
            TAILQ_INSERT_TAIL(base->activequeues[ev->ev_pri], ev, ev_active_next);
            break;
        case EVLIST_TIMEOUT: // 定时事件,加入堆
            min_heap_push(&base->timeheap, ev);
            break;
    }
}

  当事件激活(即所监听的事件发生),该事件会被插入激活队列,随后激活队列中的事件被处理(事件处理函数被调用),这部分工作在事件循环中完成:

事件循环的函数调用主要逻辑

  1. dispatch 函数调用 epoll_wait(只讨论使用了 epoll 的情况) 等待 I/O 事件发生
  2. epoll_wait 返回后,处理信号事件(epoll_wait 返回有可能是因为发生了信号事件,这和 Libevent 对信号事件的管理有关,详见信号事件管理)
  3. 将就绪的 I/O 事件添加到激活事件队列
  4. 将超时事件添加到激活事件队列
  5. event_process_active 统一处理激活事件队列中的事件(即调用每个事件对应的事件处理函数)

删除事件

int event_del(struct event *ev);

int event_del(struct event *ev)
{
    struct event_base *base;
    const struct eventop *evsel;
    void *evbase;
    
    // ev_base为NULL,表明ev没有被注册
    if (ev->ev_base == NULL)
        return (-1);
    
    // 取得ev注册的event_base和eventop指针
    base = ev->ev_base;
    evsel = base->evsel;
    evbase = base->evbase;
    // 将ev_callback调用次数设置为
    if (ev->ev_ncalls && ev->ev_pncalls) {
        *ev->ev_pncalls = 0;
    }
    // 从对应的链表中删除
    if (ev->ev_flags & EVLIST_TIMEOUT)
        event_queue_remove(base, ev, EVLIST_TIMEOUT);
    if (ev->ev_flags & EVLIST_ACTIVE)
        event_queue_remove(base, ev, EVLIST_ACTIVE);
    if (ev->ev_flags & EVLIST_INSERTED) {
        event_queue_remove(base, ev, EVLIST_INSERTED);
        // EVLIST_INSERTED表明是I/O或者Signal事件,
        // 需要调用I/O demultiplexer注销事件
        return (evsel->del(evbase, ev));
    }
    return (0);
}
  1. 该函数将删除事件ev,对于I/O事件,从I/O 的demultiplexer上将事件注销;对于Signal事件,将从Signal事件链表中删除;对于定时事件,将从堆上删除;
  2. 同样删除事件的操作则不一定是原子的,比如删除时间事件之后,有可能从系统I/O机制中注销会失败。

二、event

  libevent中的事件处理器是event结构类型。event结构体封装了句柄、事件类型、回调函数、以及其他必要的标志和数据.

struct event {
    TAILQ_ENTRY (event) ev_next;
    TAILQ_ENTRY (event) ev_active_next;
    TAILQ_ENTRY (event) ev_signal_next;
    unsigned int min_heap_idx; /* for managing timeouts */
    struct event_base *ev_base;
    int ev_fd;
    short ev_events;
    short ev_ncalls;
    short *ev_pncalls; /* Allows deletes in callback */
    struct timeval ev_timeout;
    int ev_pri; /* smaller numbers are higher priority */
    void (*ev_callback)(int, short, void *arg);
    void *ev_arg;
    int ev_res; /* result passed to event callback */
    int ev_flags;
};
  1. ev_events:所支持的事件类型,可以用所示的标志按位与(互斥的标志不行,如读写事件和信号不能同时设置)
    #define EV_TIMEOUT 0x01//定时事件
    #define EV_READ 0x02//可读事件——I/O事件
    #define EV_WRITE 0x04//可写事件——I/O事件
    #define EV_SIGNAL 0x08//信号事件
    #define EV_PERSIST 0x10//永久事件,事件被触发后自动重新对这个event调用event_add
  2. ev_next:libevent使用双向链表保存所有注册的I/O和Signal事件,ev_next就是该I/O事件在链表中的位置;称此链表为“已注册事件链表”;ev_signal_next就是signal事件在signal事件链表中的位置;
    #define TAILQ_ENTIRY(type)\
    struct\
    {
        struct type *tqe_next;\ /*下一个元素*/
        struct type **tqe_prev;\ /*前一个元素的地址*/
    }
    View Code
  3. ev_active_next:激活事件链表,libevent将所有的激活事件放入到链表active list中,然后遍历active list执行调度,ev_active_next就指明了event在active list中的位置;
  4. min_heap_idx和ev_timeout,如果是timeout事件,它们是event在小根堆中的索引和超时值,libevent使用小根堆来管理定时事件
  5. ev_base该事件所属的反应堆实例,这是一个event_base结构体
  6. ev_fd,对于I/O事件,是绑定的文件描述符;对于signal事件,是绑定的信号
  7. ev_callback,event的回调函数,被ev_base调用,执行事件处理程序,这是一个函数指针,原型为:
    void (*ev_callback)(int fd, short events, void *arg);
    //fd:ev_fd
    //events:ev_events
    //arg:ev_arg,表明可以是任意类型的数据,在设置event时指定;
  8. _ev:所有相同描述符值的I/O事件处理器通过ev.ev_io.ev_io_next成员串成一个尾队列,称为I/O事件队列,所有相同信号值得事件处理器通过ev.ev_signal.ev_signal_next成员串成一个尾队列(信号事件队列),ev_ev_signal.ev_ncalls成员指定信号发生时,Reactor需要执行多少次该事该事件对应的件处理器中的回调函数,ev_ev_signal.ev_pncalls要么是NULL要么是指向ev_ev_signal.ev_ncalls.
  9. ev_ncalls:事件就绪执行时,调用ev_callback的次数,通常为1
  10. ev_pncalls:指针,通常指向ev_ncalls或者为NULL;
  11. ev_res:记录了当前激活事件的类型;
  12. eb_flags:libevent用于标记event信息的字段,表明其当前的状态,可能的值有
    #define EVLIST_TIMEOUT 0x01 // event在time堆中
    #define EVLIST_INSERTED 0x02 // event在已注册事件链表中
    #define EVLIST_SIGNAL 0x04 // 未见使用
    #define EVLIST_ACTIVE 0x08 // event在激活链表中
    #define EVLIST_INTERNAL 0x10 // 内部使用标记
    #define EVLIST_INIT 0x80 // event已被初始化

  每次当有事件event转变为就绪状态时,libevent就会把它移入到active event list[priority]中,其中priority是event的优先级;接着libevent会根据自己的调度策略选择就绪事件,调用其cb_callback()函数执行事件处理;并根据就绪的句柄和事件类型填充cb_callback函数的参数。

三、设置事件的接口

  设置事件ev绑定的文件描述符或者信号,对于定时事件,设为-1即可

void event_set(struct event *ev, int fd, short events,void (*callback)(int, short, void *), void *arg);
  1. 设置事件类型,比如EV_READ|EV_PERSIST, EV_WRITE, EV_SIGNAL等;
  2. 设置事件的回调函数以及参数arg;
  3. 初始化其它字段,比如缺省的event_base和优先级;

  设置event ev将要注册到的event_base

int event_base_set(struct event_base *base, struct event *ev);

  libevent有一个全局event_base指针current_base,默认情况下事件ev将被注册到current_base上,使用该函数可以指定不同的event_base;如果一个进程中存在多个libevent实例,则必须要调用该函数为event设置不同的event_base;

  设置event ev的优先级,没什么可说的,注意的一点就是:当ev正处于就绪状态时,不能设置,返回-1

int event_priority_set(struct event *ev, int pri);

四、统一事件源

I/O和timer事件统一

  首先将Timer事件融合到系统I/O多路复用机制中,还是相当清晰的,因为系统的I/O机制像select()和epoll_wait()都允许程序制定一个最大等待时间(也称为最大超时时间)timeout,即使没有I/O事件发生,它们也保证能在timeout时间内返回。

  那么根据所有Timer事件的最小超时时间来设置系统I/O的timeout时间;当系统I/O返回时,再激活所有就绪的Timer事件就可以了,这样就能将Timer事件完美的融合到系统的I/O机制中了。

I/O和signal事件统一

  Signal是异步事件的经典事例,将Signal事件统一到系统的I/O多路复用中就不像Timer事件那么自然了,Signal事件的出现对于进程来讲是完全随机的,进程不能只是测试一个变量来判别是否发生了一个信号,而是必须告诉内核“在此信号发生时,请执行如下的操作”。

  如果当Signal发生时,并不立即调用event的callback函数处理信号,而是设法通知系统的I/O机制,让其返回,然后再统一和I/O事件以及Timer一起处理,当signal事件发生时,可以使用pipe通知I/O多路复用机制。

五、集成信号处理事件

集成策略——使用socket pair

创建sockpair流程

集成到事件主循环——通知event_base

  Socket pair创建好了,可是libevent的事件主循环还是不知道Signal是否发生了,看来我们还差了最后一步,那就是:为socket pair的读socket在libevent的event_base实例上注册一个persist的读事件。
这样当向写socket写入数据时读socket就会得到通知,触发读事件,从而event_base就能相应的得到通知了。

  前面提到过,Libevent会在事件主循环中检查标记,来确定是否有触发的signal,如果标记被设置就处理这些signal,这段代码在各个具体的I/O机制中,以Epoll为例,在epoll_dispatch()函数中,代码片段如下

    res = epoll_wait(epollop->epfd, events, epollop->nevents, timeout);
    if (res == -1) {
        if (errno != EINTR) {
            event_warn("epoll_wait");
            return (-1);
        }
        evsignal_process(base);// 处理signal事件
        return (0);
    } 
    else if (base->sig.evsignal_caught) {
        evsignal_process(base);// 处理signal事件
    }

  1. libevent中,初始化阶段并不注册读socket的读事件,而是在注册信号阶段才会测试并注册;
  2. libevent中,检查I/O事件是在各系统I/O机制的dispatch()函数中完成的,该dispatch()函数在event_base_loop()函数中被调用;

  注册signal事件是通过evsignal_add(struct event *ev)函数完成的,libevent对所有的信号注册同一个处理函数evsignal_handler(),该函数将在下一段介绍,注册过程如下:

  1. 取得ev要注册到的信号signo;
  2. 如果信号signo未被注册,那么就为signo注册信号处理函数evsignal_handler();
  3. 如果事件ev_signal还没哟注册,就注册ev_signal事件;
  4. 将事件ev添加到signo的event链表中;

  从signo上注销一个已注册的signal事件就更简单了,直接从其已注册事件的链表中移除即可。如果事件链表已空,那么就恢复旧的处理函数;

  下面的讲解都以signal()函数为例,sigaction()函数的处理和signal()相似。处理函数evsignal_handler()函数做的事情很简单,就是记录信号的发生次数,并通知event_base有信号触发,需要处理:

static void evsignal_handler(int sig)
{
    int save_errno = errno; // 不覆盖原来的错误代码
    if (evsignal_base == NULL) {
        event_warn("%s: received signal %d, but have no base configured", __func__, sig);
        return;
    }
    // 记录信号sig的触发次数,并设置event触发标记
    evsignal_base->sig.evsigcaught[sig]++;
    evsignal_base->sig.evsignal_caught = 1;
    #ifndef HAVE_SIGACTION
        signal(sig, evsignal_handler); // 重新注册信号
    #endif
    // 向写socket写一个字节数据,触发event_base的I/O事件,从而通知其有信号触发,需要处理
    send(evsignal_base->sig.ev_signal_pair[0], "a", 1, 0);
    errno = save_errno; // 错误代码
}

六、集成定时事件

  因为系统的I/O机制像select()和epoll_wait()都允许程序制定一个最大等待时间(也称为最大超时时间)timeout,即使没有I/O事件发生,它们也保证能在timeout时间内返回。那么根据所有Timer事件的最小超时时间来设置系统I/O的timeout时间;当系统I/O返回时,再激活所有就绪的Timer事件就可以了,这样就能将Timer事件完美的融合到系统的I/O机制中了。

  具体的代码在源文件event.c的event_base_loop()中

    if (!base->event_count_active && !(flags & EVLOOP_NONBLOCK)) {
        // 根据Timer事件计算evsel->dispatch的最大等待时间
        timeout_next(base, &tv_p);
    } else {
        // 如果还有活动事件,就不要等待,让evsel->dispatch立即返回
        evutil_timerclear(&tv);
    }
    // ...
    // 调用select() or epoll_wait() 等待就绪I/O事件
    res = evsel->dispatch(base, evbase, tv_p);
    // ...
    // 处理超时事件,将超时事件插入到激活链表中
    timeout_process(base);
        
    
    //timeout_next()函数根据堆中具有最小超时值的事件和当前时间来计算等待时间
    static int timeout_next(struct event_base *base, struct timeval **tv_p)
    {
        struct timeval now;
        struct event *ev;
        struct timeval *tv = *tv_p;
        // 堆的首元素具有最小的超时值
        if ((ev = min_heap_top(&base->timeheap)) == NULL) {
            // 如果没有定时事件,将等待时间设置为NULL,表示一直阻塞直到有I/O事件发生
            *tv_p = NULL;
            return (0);
        }
        // 取得当前时间
        gettime(base, &now);
        // 如果超时时间<=当前值,不能等待,需要立即返回
        if (evutil_timercmp(&ev->ev_timeout, &now, <=)) {
            evutil_timerclear(tv);
            return (0);
        }
        // 计算等待的时间=当前时间-最小的超时时间
        evutil_timersub(&ev->ev_timeout, &now, tv);
        return (0);
    }

Timer小根堆

  Libevent使用堆来管理Timer事件,其key值就是事件的超时时间

    Heap[size++] <- new; // 先放到数组末尾,元素个数+1
    // 下面就是shift_up()的代码逻辑,不断的将new向上调整
    _child = size;
    while(_child>0) // 循环
    {
        _parent 􀃅 (_child-1)/2; // 计算parent
        if(Heap[_parent].key < Heap[_child].key)
            break; // 调整结束,跳出循环
        swap(_parent, _child); // 交换parent和child
    }

  而libevent的heap代码对这一过程做了优化,在插入新元素时,只是为新元素预留了一个位置hole(初始时hole位于数组尾部),但并不立刻将新元素插入到hole上,而是不断向上调整hole的值,将父节点向下调整,最后确认hole就是新元素的所在位置时,才会真正的将新元素插入到hole上,因此在调整过程中就比上面的代码少了一次赋值的操作,代码逻辑是:

    // 下面就是shift_up()的代码逻辑,不断的将new的“预留位置”向上调整
    _hole = size; // _hole就是为new预留的位置,但并不立刻将new放上
    while(_hole>0) // 循环
    {
        _parent 􀃅 (_hole-1)/2; // 计算parent
        if(Heap[_parent].key < new.key)
            break; // 调整结束,跳出循环
        Heap[_hole] = Heap[_parent]; // 将parent向下调整
        _hole = _parent; // 将_hole调整到_parent
        }
    Heap[_hole] = new; // 调整结束,将new插入到_hole指示的位置
    size++; // 元素个数+1

七、支持I/O多路复用

struct eventop {
    const char *name;
    void *(*init)(struct event_base *); // 初始化
    int (*add)(void *, struct event *); // 注册事件
    int (*del)(void *, struct event *); // 删除事件
    int (*dispatch)(struct event_base *, void *, struct timeval *); // 事件分发
    void (*dealloc)(struct event_base *, void *); // 注销,释放资源
    /* set if we need to reinitialize the event base */
    int need_reinit;
};

  Libevent把所有支持的I/O demultiplex机制存储在一个全局静态数组eventops中,并在初始化时选择使用何种机制或者可以自定义,数组内容根据优先级顺序声明如下:

/* In order of preference */
static const struct eventop *eventops[] = {
    #ifdef HAVE_EVENT_PORTS
        &evportops,
    #endif
    #ifdef HAVE_WORKING_KQUEUE
        &kqops,
    #endif
    #ifdef HAVE_EPOLL
        &epollops,
    #endif
    #ifdef HAVE_DEVPOLL
        &devpollops,
    #endif
    #ifdef HAVE_POLL
        &pollops,
    #endif
    #ifdef HAVE_SELECT
        &selectops,
    #endif
    #ifdef WIN32
        &win32ops,
    #endif
    NULL
};

  然后libevent根据系统配置和编译选项决定使用哪一种I/O demultiplex机制,这段代码在函数event_base_new()中:

base->evbase = NULL;
for (i = 0; eventops[i] && !base->evbase; i++) {
    base->evsel = eventops[i];
    base->evbase = base->evsel->init(base);
}

  可以看出,libevent在编译阶段选择系统的I/O demultiplex机制,而不支持在运行阶段根据配置再次选择

  以Linux下面的epoll为例,实现在源文件epoll.c中,eventops对象epollops定义如下:

const struct eventop epollops = {
    "epoll",
    epoll_init,
    epoll_add,
    epoll_del,
    epoll_dispatch,
    epoll_dealloc,
    1 /* need reinit */
};

  变量epollops中的函数指针具体声明如下,注意到其返回值和参数都和eventop中的定义严格一致,这是函数指针的语法限制。

static void *epoll_init (struct event_base *);
static int epoll_add (void *, struct event *);
static int epoll_del (void *, struct event *);
static int epoll_dispatch(struct event_base *, void *, struct timeval *);
static void epoll_dealloc (struct event_base *, void *);

  那么如果选择的是epoll,那么调用结构体eventop的init和dispatch函数指针时,实际调用的函数就是epoll的初始化函数epoll_init()和事件分发函数epoll_dispatch()了;

八、libevent支持多线程

错误示例

在多核的CPU上只使用一个线程始终是对不起CPU的处理能力啊,那好吧,那就多创建几个线程,比如下面的简单服务器场景。

  1. 主线程创建工作线程1;
  2. 接着主线程监听在端口上,等待新的连接;
  3. 在线程1中执行event事件循环,等待事件到来;
  4. 新连接到来,主线程调用libevent接口event_add将新连接注册到libevent上;
  5. … …

  上面的逻辑看起来没什么错误,在很多服务器设计中都可能用到主线程和工作线程的模式….可是就在线程1注册事件时,主线程很可能也在操作事件,比如删除,修改,通过libevent的源代码也能看到,没有同步保护机制

消息通知+同步层

  有个折中机制可以减少消息通信的开销,就是提取一个同步层,还拿上面的例子来说,你把工作安排都存放在一个工作队列中,而且你能够保证“任何人把新任务扔到这个队列”,“自己取出当前第一个任务”等这些操作都能够保证不会把队列搞乱(其实就是个加锁的队列容器)。

libevent官方文档:https://www.monkey.org/~provos/libevent/doxygen-2.0.1/include_2event2_2event_8h.html

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!