libevent源码学习(12):超时管理之common_timeout

2023-11-06

目录

前言

common_timeout的作用

common_timeout的结构定义

common_timeout与一般timeout的区分

获取common_timeout在common_timeout_queues中的下标

判断一个timeval是否为common_timeout

判断两个timeval是否是同样的common_timeout

获取common_timeout对应的common_timeout_list

创建一个common_timeout

为event添加common_timeout

从common_timeout_list到min_heap

激活common_timeout对应的event

总结


以下源码均基于libevent-2.0.21-stable。

前言

       用于超时管理的min_heap,在执行主循环的过程中,它每次都会去检查min_heap的堆顶event是否超时,如果超时的话就进行相应的处理并且从min_heap中移除这个event,然后调整整个堆,直到堆顶event未超时则停止检查。这种方法虽然好,逻辑清晰,看上去每次删除堆顶超时的event时间复杂度只需要O(logn),效率也足够高,但是如果某次主循环中超时的event过多,假设有m个event超时了需要同时处理,那么此时需要花费的时间就是O(mlogn),当m足够多的时候,这个效率还是比较低的,因此就引入了common_timeout这一结构。

       那么它的作用是什么呢?

common_timeout的作用

       简单来说,common_timeout把base中所有拥有共同点的event放在了一起,而这个所谓的“共同点”就是指超时时长相同,这些超时时长相同的event,他们的超时时间是不同的。

       举个例子,我添加了一个eventA,设置它的超时时长为5分钟,即如果5分钟内没有触发相应事件,那么5分钟后就直接进行回调处理;然后我再添加了一个eventB,也设置它的超时时长为5分钟。那么就称eventA和eventB具有相同的超时时长。如果eventA添加的时间为10:00,eventB添加的时间为11:00,那么二者的超时时间就一个是10:05,另一个就是11:05,因此超时时长相同,但是超时时间是不同的。

       拥有相同超时时长的所有event构成一个链表events,并且让它们按照超时时间的先后按升序排列(即相同超时时长中最先超时的那个event放在最前面),而events中设置一个内部使用的timeout_event作为代表,把最先超时的那个event的超时时间添加到timeout_event中,然后把timeout_event放到min_heap中,当放到min_heap中的timeout_event超时,就回到events中,从前往后把所有超时的event全部激活。下面来分析这种情况下的时间复杂度。

       在这种情况下,相当于每一个由相同超时时长的event组成的链表都在min_heap中存在一个“代表”,因此如果有t个链表的“代表”在min_heap中超时,那么处理这个"代表"后调整堆花费的时间就是O(tlogn),如果一共有m个event超时了,相当于所有链表加起来需要遍历m个event,因此common_timeout在处理m个event超时的时间复杂度就是O(tlogn+m),由此也能看出来使用common_timeout+min_heap和只使用min_heap的差别了:如果t远小于m,相当于超时的event分布的链表比较集中,那么前者的效率更高,这种优势当n越大时越明显;如果t和m相近,相当于超时的event分布在不同的链表,此时还是后者效率更高。

       因此到底是否使用common_timeout,还是视情况而定,这也是为什么在libevent中虽然设计了common_timeout,但是并没有将其直接用来管理超时,而是留给用户接口去选择是否使用common_timeout,可见,common_timeout+min_heap的超时管理并非就一定比只使用min_heap的效率高。而至于具体在什么情况下使用哪种方式,个人觉得如果超时的event很多那还是应该考虑使用common_timeout+min_heap,因为event很多的话分布的链表也更大概率密集一些;如果超时的event比较少的话,还是应该只使用min_heap。

common_timeout的结构定义

       在event_base的结构体中,含有以下定义:

struct event_base
{
    ......

    /** An array of common_timeout_list* for all of the common timeout
	 * values we know. */
	struct common_timeout_list **common_timeout_queues;   //common_timeout_list *数组,存放不同超时时长的common_timeout_list的指针
	/** The number of entries used in common_timeout_queues */
	int n_common_timeouts;  //common_timeout_queues中实际的元素个数
	/** The total size of common_timeout_queues. */
	int n_common_timeouts_allocated;  //common_timeout_queues的容量
    ......
}

         在event_base中,common_timeout_queues是一个common_timeout_list *类型的指针数组,其中每个元素都指向一个common_timeout_list,common_timeout_list的定义如下:

//event-internal.h
struct common_timeout_list {
	/* List of events currently waiting in the queue. */
	struct event_list events;  //event的双向链表
	/* 'magic' timeval used to indicate the duration of events in this
	 * queue. */
	struct timeval duration;  //该common_timeout_list的超时时长,events双向链表中的所有event都是相同的超时时长
	/* Event that triggers whenever one of the events in the queue is
	 * ready to activate */
	struct event timeout_event;  //“event代表”,最终只有这个event实际插到了min_heap中
	/* The event_base that this timeout list is part of */
	struct event_base *base;  //该common_timeout_list所在的event_base
};

//event_struct.h

TAILQ_HEAD (event_list, event);  //由event组成的双向链表

        从上述二者的定义可以知道,每一个event_base都对应一个common_timeout_list *的数组common_timeout_queues,它其中每个元素都指向一个common_timeout_List,每一个common_timeout_list都指明了其对应的超时时长(duration),以及超时时长相同都等于duration的所有event组成的双向链表(events)。此外就是timeout_event,这个event最终会作为“代表”插入到min_heap中,实际min_heap处理的超时event也是timeout_event。

        举个例子,假如common_timeout_queues[0]对应的common_timeout_list的duration为3s,那么这个common_timeout_list的events双向链表中就会存放所有超时时长为3s的event。不过这必须保证这里设置的“3s”必须是common_timeout的3s而不是普通超时结构体的3s。

       那么,如何来区分一个超时结构体是common_timeout还是普通的超时结构体呢?

common_timeout与一般timeout的区分

       对于一个timeval超时结构体来说,它有两个成员,一个数tv_sec用来指明超时时间中的秒数,一个就是tv_usec用来指明超时时间中的微秒数。由于微秒的数值范围只能是0~999999,而tv_usec的变量类型实际上是32位的,能表示的数值范围远远大于999999,因此用低20位足以来表示timeval中的tv_usec,这样一来,tv_usec的高12位就是没有使用的。而libevent中则是通过这高12位来区分一个timeval超时结构体是common_timeout还是普通的timeout。有如下定义:

//event-internal.h
#define COMMON_TIMEOUT_MICROSECONDS_MASK       0x000fffff   //取低20位掩码

//event.c
#define MICROSECONDS_MASK       COMMON_TIMEOUT_MICROSECONDS_MASK   //取低20位,即微秒超时时长
#define COMMON_TIMEOUT_IDX_MASK 0x0ff00000   //20~27bit为该超时时长在common_timeout_queues中的位置
#define COMMON_TIMEOUT_IDX_SHIFT 20          //微秒最大为999999,因此用低20位存储即可,高12位中前4位标识是否为common_timeout 
#define COMMON_TIMEOUT_MASK     0xf0000000   //取高四位掩码
#define COMMON_TIMEOUT_MAGIC    0x50000000   //高四位标志是否为common timeout

        这里定义了6个宏定义,其中前两个是相同的。timeval结构体中的tv_usec由32位表示,而实际上微秒的数值只需低20位即可表示,因此,tv_usec & MICROSECONDS_MASK  即可得到低20位的值,也就是实际的微秒数值。

        对于高12位来说,tv_usec的高4位用来判断一个这个timeval是否是common_timeout,因为用 tv_usec & COMMON_TIMEOUT_MASK就可以屏蔽掉除高4位以外的其他位,在libevent中规定,如果屏蔽掉低位后得到的值刚好是COMMON_TIMEOUT_MAGIC(即0x5000000),那么就说明这个timeval是一个common_timeout,否则就表示这个timeval只是一个普通的超时结构体。

        前面说过,如果是common_timeout,那么这个event是放在base的common_timeout_queues某一项(如common_timeout_queues[i])所对应的common_timeout_list中的,而tv_usec剩下的20~27bit则用来表示这个common_timeout所在的common_timeout_list在common_timeout_queues数组中的索引。举个例子,如果tv_usec的20~27bit为00000101,则说明这个tv_usec对应的common_timeout_list放在common_timeout_queues[5]下面。

        知道了common_timeout的定义,就可以有以下辅助函数了:

获取common_timeout在common_timeout_queues中的下标

#define COMMON_TIMEOUT_IDX(tv) \ //获取tv所在的common_timeout_list在common_timeout_queues中的位置
	(((tv)->tv_usec & COMMON_TIMEOUT_IDX_MASK)>>COMMON_TIMEOUT_IDX_SHIFT)//20~27bit右移20位得到下标

        这里定义的是一个宏函数。将tv_usec与COMMON_TIMEOUT_IDX_MASK按位与,屏蔽掉tv_usec除20~27bit以外的其他位,得到的结果再右移20位得到的结果就是原来的tv_usec的20~27bit的值,这个值就是common_timeout对应的common_timeout_list对应在common_timeout_queues中的下标。

判断一个timeval是否为common_timeout

static inline int
is_common_timeout(const struct timeval *tv,
    const struct event_base *base)
{
	int idx;
	if ((tv->tv_usec & COMMON_TIMEOUT_MASK) != COMMON_TIMEOUT_MAGIC)//取高4位,COMMON_TIMEOUT_MAGIC说明它是一个common timeout,如果高四位不等于COMMON_TIMEOUT_MAGIC,那么就不是commontimeout
		return 0;
	idx = COMMON_TIMEOUT_IDX(tv);
	return idx < base->n_common_timeouts; //下标必须小于 base中common_timeout_queues的实际元素个数
}

       该函数先取出tv_usec的高四位来检查传入的timeval是否为common_timeout,在此基础上还要去判断tv_usec的20~27bit所对应的下标是否合法,因为有可能存在高四位表明为common_timeout,但是20~27bit是非法的情况。

判断两个timeval是否是同样的common_timeout

static inline int
is_same_common_timeout(const struct timeval *tv1, const struct timeval *tv2)
{
	return (tv1->tv_usec & ~MICROSECONDS_MASK) ==
	    (tv2->tv_usec & ~MICROSECONDS_MASK);   //比较的实际上是20~31bit,如果相同则说明两个超时共用同一个common timeout
}

获取common_timeout对应的common_timeout_list

static inline struct common_timeout_list *  //获得tv所在的common_timeout_list指针
get_common_timeout_list(struct event_base *base, const struct timeval *tv)
{
	return base->common_timeout_queues[COMMON_TIMEOUT_IDX(tv)];  
}

创建一个common_timeout

       前面已经提到,如果想要创建一个common_timeout,首先就必须在base的common_timeout_queues中有相应的common_timeout_list去存放这个common_timeout所对应的event,也就是说,创建一个common_timeout就必须在base中有相应的空间;第二点则是,common_timeout与一般的timeval不同,其tv_usec中的高12位存放的都是common_timeout相关的信息,如果想把一个普通的timeval,那么就必须对timeval的tv_usec高12位进行修改才行,而这种修改不应该由用户手动修改,这样显得太麻烦了。

       以上两点,是创建一个common_timeout的条件,在libevent中则提供了一个函数去做这些事:

const struct timeval *
event_base_init_common_timeout(struct event_base *base,
    const struct timeval *duration) //查看base中是否有duration相应的common_timeout_list,如果没有就分配一个,并且将新分配中的timeout_event进行设置回调函数。传入的duration既可以是带掩码的也可以是不带掩码的,返回的是相应的common_timeout_list的duration
{
	int i;
	struct timeval tv;
	const struct timeval *result=NULL;
	struct common_timeout_list *new_ctl;

	EVBASE_ACQUIRE_LOCK(base, th_base_lock);
	if (duration->tv_usec > 1000000) { //微秒最大值应该是999999,如果超过了1000000,要么它是一个common_timeout,就取出实际的超时时间,否则就把微秒进位到秒上去
		memcpy(&tv, duration, sizeof(struct timeval));
		if (is_common_timeout(duration, base))
			tv.tv_usec &= MICROSECONDS_MASK;
		tv.tv_sec += tv.tv_usec / 1000000;
		tv.tv_usec %= 1000000;
		duration = &tv;   //更新duration的实际时长
	}
	for (i = 0; i < base->n_common_timeouts; ++i) { //遍历现在有的common_timeout_list,查看是否存在超时时长等于duration的list
		const struct common_timeout_list *ctl =
		    base->common_timeout_queues[i];
		if (duration->tv_sec == ctl->duration.tv_sec &&
		    duration->tv_usec ==
		    (ctl->duration.tv_usec & MICROSECONDS_MASK)) {
			EVUTIL_ASSERT(is_common_timeout(&ctl->duration, base));
			result = &ctl->duration; //如果存在duration等于传入的参数的common_timeout_list,那么就把这个common_timeout_list的duration存到result中返回即可。
			goto done;
		}
	}
	
	if (base->n_common_timeouts == MAX_COMMON_TIMEOUTS) {
		event_warnx("%s: Too many common timeouts already in use; "
		    "we only support %d per event_base", __func__,
		    MAX_COMMON_TIMEOUTS);
		goto done;
	}
	if (base->n_common_timeouts_allocated == base->n_common_timeouts) { //如果base中的common_timeout_list分配满了
		int n = base->n_common_timeouts < 16 ? 16 : //如果少于16则分配16的容量,否则容量加倍
		    base->n_common_timeouts*2;
		struct common_timeout_list **newqueues =
		    mm_realloc(base->common_timeout_queues,
			n*sizeof(struct common_timeout_queue *));//重新分配common_timeout_queues的空间大小
		if (!newqueues) {
			event_warn("%s: realloc",__func__);
			goto done;
		}
		base->n_common_timeouts_allocated = n;  //更新common_timeout_queues地址及其容量
		base->common_timeout_queues = newqueues;
	}
    //执行到这里说明没有common_timeout_list的duration等于传入的参数duration
	new_ctl = mm_calloc(1, sizeof(struct common_timeout_list));   //新分配一个common_timeout_list
	if (!new_ctl) {
		event_warn("%s: calloc",__func__);
		goto done;
	}
	TAILQ_INIT(&new_ctl->events); //初始化该duration对应的events链表为空
	new_ctl->duration.tv_sec = duration->tv_sec;
	new_ctl->duration.tv_usec =
	    duration->tv_usec | COMMON_TIMEOUT_MAGIC |
	    (base->n_common_timeouts << COMMON_TIMEOUT_IDX_SHIFT);  //把微秒转换为为带掩码、并且添上下标位
	evtimer_assign(&new_ctl->timeout_event, base,
	    common_timeout_callback, new_ctl); //给新分配的common_timeout_list中的timeout_event注册信息,回调函数为common_timeout_callback
	new_ctl->timeout_event.ev_flags |= EVLIST_INTERNAL;  //标志为内部使用的event
	event_priority_set(&new_ctl->timeout_event, 0);  //设置timeout_event的优先级为0
	new_ctl->base = base;
	base->common_timeout_queues[base->n_common_timeouts++] = new_ctl; //在common_timeout_queues现有元素的最后加上新创建的common_timeout_list
	result = &new_ctl->duration;  //result保存common_timeout_list的duration

done:
	if (result)
		EVUTIL_ASSERT(is_common_timeout(result, base));

	EVBASE_RELEASE_LOCK(base, th_base_lock);
	return result;  //返回的result是已经设置过相应标志位的common_timeout,也就是指定了超时时长为duration的common_timeout,之后就可以用result作为参数调用event_add,就可以把event添加到相应的common_timeout_list中
}

        传入该函数的参数一个是base,这没有什么可说的,另一个就是duration。这里的duration是什么意思呢?比如说你想在后面为一个event添加一个超时时长为3s的common_timeout,那么你就需要先把3s作为这里的duration参数去调用event_base_init_common_timeout函数,这个函数会在base的common_timeout_queues中去寻找超时时长为3s的common_timeout_list,common_timeout_list中的duration成员描述了该common_timeout_list对应的超时时长。

        如果找到了,那么就直接把这个common_timeout_list的duration存到result中返回,需要注意的是,这里common_timeout_list的duration和传入的3s的duration是不同的,传入的duration只是你单纯想要的一个3s,而传出的duration则是在这个3s的基础上设置好了高12位的common_timeout。

        如果没找到,那么就会重新创建一个common_timeout_list并将其放到base的common_timeout_queues中,然后把传入的3s的timeval的tv_usec加上高12位的标志位作为新建的common_timeout_list的duration,并且将这个duration存到result中。

        除此之外,对于新建的common_timeout_list,还会通过event_assign来对的timeout_event成员进行注册,为这个timeout_event添加回调函数等信息,如下所示:

evtimer_assign(&new_ctl->timeout_event, base,common_timeout_callback, new_ctl); 

//event.h

#define evtimer_assign(ev, b, cb, arg)  event_assign((ev), (b), -1, 0, (cb), (arg))

       这个timeout_event最终会被作为整个common_timeout_list上所有event的“代表”添加到min_heap中。

       调用该函数后,返回的timeval实际上是时间设置与传入的duration相同,但是变成了common_timeout,这样如果你后面想为一个event添加一个3s中的common_timeout,就可以用返回的timeval作为超时参数。

为event添加common_timeout

       通过event_add函数可以向base中添加一个监听事件event,并且设置监听超时timeval,该函数内部实际上是调用的event_add_internal实现相应功能的。下面来看看该函数是如何为一个event添加一个common_timeout的。相关代码如下所示:

static inline int
event_add_internal(struct event *ev, const struct timeval *tv,
    int tv_is_absolute)  //根据event的类型events来将其放到对应的queue中,并且设置相应的flag,如果还设置了超时,就将event的超时结构体放到定时器堆中
{   
        ......

		gettime(base, &now); //获取系统时间

		common_timeout = is_common_timeout(tv, base);
		if (tv_is_absolute) {  //如果是绝对时间 就直接用ev_timeout存储
			ev->ev_timeout = *tv;
		} else if (common_timeout) {   //如果不是绝对时间,是common_timeout的话就要处理一下高12位的标志
			struct timeval tmp = *tv;
			tmp.tv_usec &= MICROSECONDS_MASK;  //传入的相对时间实际上只是低20位
			evutil_timeradd(&now, &tmp, &ev->ev_timeout);  //将绝对时间加上相对时间,作为超时时间
			ev->ev_timeout.tv_usec |=
			    (tv->tv_usec & ~MICROSECONDS_MASK);//将tv->tv_usec的高12位添加到ev_timeout中
		} else {
			evutil_timeradd(&now, tv, &ev->ev_timeout); //如果就只是一个普通的相对时间,就直接用绝对时间加上该值作为超时时间
		}
		//执行到这里,ev->ev_timeout中存放的就是绝对时间了。
		event_debug((
			 "event_add: timeout in %d seconds, call %p",
			 (int)tv->tv_sec, ev->ev_callback));

		event_queue_insert(base, ev, EVLIST_TIMEOUT); //插入到超时队列中,如果是common_timeout就插入到相应的common_timeout_List按升序排列的events的相应位置,否则就直接插入到min_heap中
		

        if (common_timeout) {//如果是common_timeout,仅当event是common_timeout_list中的第一项会将其插入到min_heap中
			struct common_timeout_list *ctl =
			    get_common_timeout_list(base, &ev->ev_timeout); //得到超时时间所对应的common_timeout_list
			if (ev == TAILQ_FIRST(&ctl->events)) { //判断ev是否是其对应的common_timeout_list的第一项
				common_timeout_schedule(ctl, &now, ev); //如果ev是第一项,说明它就是整个list中最先超时的那个event,就用该event的超时时间作为参数
			}
		} 
		......
}

       可以把这段程序按照event_queue_insert(base, ev, EVLIST_TIMEOUT);一句分为上下两部分。对于上面一部分,会先获取当前的系统时间到now中(这里的系统时间指的是从1970年1月1日到现在的时间),由于event的ev_timeout成员中存储的超时是相对于系统时间来说的绝对超时时长,而我们传入的tv一般来说都是相对超时时长,因此如果传入的tv是common_timeout(非绝对时间),那么就会先将tv的tv_usec微秒定时与掩码进行按位与,那么得到的tv就是非不带common_timeout掩码的相对超时时长,然后将这个超时时长与当前的系统时间求和,得到的就是绝对超时时间保存到ev_timeout中。也就是当系统时间到达ev_timeout的时间,相应事件才会超时。然后将ev_timeout的超时时间做好之后,就可以通过ev->ev_timeout.tv_usec |=(tv->tv_usec & ~MICROSECONDS_MASK);来给ev_timeout添加common_timeout的12位标志。

       因此上面部分代码的主要作用是:如果传入的tv是common_timeout,那么就先根据这个common_timeout取出它的实际超时时长,然后将这个超时时长加上系统时间得到绝对超时时间,并将其保存到event的ev_timeout中并为其添加common_timeout的标志位。

       通过以上部分程序,相当于只是让event的ev_timeout保存了这个common_timeout,但是如果想让base按照common_timeout去处理这个event,那么就还需要把这个event放到common_timeout对应的common_timeout_list中,接下来调用的event_queue_insert就做了这样的事情:

static void 
event_queue_insert(struct event_base *base, struct event *ev, int queue)
{
	......
	switch (queue) {
	......
	case EVLIST_TIMEOUT: {   //如果是设置超时事件
		if (is_common_timeout(&ev->ev_timeout, base)) {
			struct common_timeout_list *ctl =
			    get_common_timeout_list(base, &ev->ev_timeout);
			insert_common_timeout_inorder(ctl, ev);
		} else
			min_heap_push(&base->timeheap, ev);
		break;
	}
	......
}

       对于common_timeout来说,这里传入的queue就是EVLIST_TIMEOUT,换句话说,该函数的目的就是将event插入到base的超时队列中。由于上面那段程序已经设置了event的ev_timeout,因此这里is_common_timeout(&ev->ev_timeout, base)就为真,紧接着获取了base中ev_timeout对应的那个common_timeout_list,并且调用了insert_common_timeout_inorder函数,从函数名和参数大概可以猜出来:这是向common_timeout_list中按序插入这个event,如下所示:

static void
insert_common_timeout_inorder(struct common_timeout_list *ctl,
    struct event *ev)//不一定就能保证后插入的超时一定比先插入的超时长,因此需要遍历events找到合适插入的地方,而由于后插的超时更大可能靠后,因此从后往前
{
	struct event *e;
	/* By all logic, we should just be able to append 'ev' to the end of
	 * ctl->events, since the timeout on each 'ev' is set to {the common
	 * timeout} + {the time when we add the event}, and so the events
	 * should arrive in order of their timeeouts.  But just in case
	 * there's some wacky threading issue going on, we do a search from
	 * the end of 'ev' to find the right insertion point.
	 */
	TAILQ_FOREACH_REVERSE(e, &ctl->events,
	    event_list, ev_timeout_pos.ev_next_with_common_timeout) {
		/* This timercmp is a little sneaky, since both ev and e have
		 * magic values in tv_usec.  Fortunately, they ought to have
		 * the _same_ magic values in tv_usec.  Let's assert for that.
		 */
		EVUTIL_ASSERT(
			is_same_common_timeout(&e->ev_timeout, &ev->ev_timeout));
		if (evutil_timercmp(&ev->ev_timeout, &e->ev_timeout, >=)) {
			TAILQ_INSERT_AFTER(&ctl->events, e, ev,
			    ev_timeout_pos.ev_next_with_common_timeout);
			return;
		}
	}
	TAILQ_INSERT_HEAD(&ctl->events, ev,
	    ev_timeout_pos.ev_next_with_common_timeout);
}

        insert_common_timeout_inorder函数的作用就是按照升序把event插到其对应的那个common_timeout_list中,这里有一点需要注意的是,把event按升序插入common_timeout_list中时是逆向遍历插入而不是正向遍历插入的,这是因为从逻辑上来说,先插入队列的event它的超时时间应该更早,后插入的event它的超时时间更晚,因此新插入的event更有可能在队列中靠后,所以逆向遍历会更快一些。

        也就是说,这里调用event_queue_insert函数实际上是把common_timeout event按升序插入到相应的common_timeout_list中。这样,就把common_timeout添加到event中,并且这个带common_timeout的event也放到了其对应的base中的相应位置。

        下面再来说说这个带common_timeout的event是如何与min_heap产生联系的。

从common_timeout_list到min_heap

          在event_queue_insert之后,还有这段代码:

static inline int
event_add_internal(struct event *ev, const struct timeval *tv,
    int tv_is_absolute) 
{   
        ......
		if (common_timeout) {//如果是common_timeout,仅当event是common_timeout_list中的第一项会将其插入到min_heap中
			struct common_timeout_list *ctl =
			    get_common_timeout_list(base, &ev->ev_timeout); //得到超时时间所对应的common_timeout_list
			if (ev == TAILQ_FIRST(&ctl->events)) { //判断ev是否是其对应的common_timeout_list的第一项
				common_timeout_schedule(ctl, &now, ev); //如果ev是第一项,说明它就是整个list中最先超时的那个event,就用该event的超时时间作为参数
			}
		} 
	......
}

       由于前面已经为event的ev_timeout进行了赋值,在common_timeout的情况下,ev_timeout中存放的是common_timeout形式的绝对超时时间。这段代码会先通过get_common_timeout_list函数取得ev_timeout在base中对应的那个common_timeout_list,然后去判断event是不是这个common_timeout_list的第一项,由于整个common_timeout_list是按照超时时间升序排列的,因此common_timeout_list的第一项肯定是最先超时的那个event。这里调用了common_timeout_schedule,来看看其定义:

static void
common_timeout_schedule(struct common_timeout_list *ctl,
    const struct timeval *now, struct event *head)
{
	struct timeval timeout = head->ev_timeout;  //获取的是第一个超时的event的超时时间
	timeout.tv_usec &= MICROSECONDS_MASK;   //取出低20位
	event_add_internal(&ctl->timeout_event, &timeout, 1);  //此时的timeout是一个非common_timeout
	//添加的其实是common_timeout_list中的那个timeout_event,这是一个内部event,设置了回调函数为timeout_callback,但是超时时间设置的是list中最先超时的那个event的超时时间
}

       先注意,调用该common_timeout_schedule的前提是前面所说的那个带common_timeout的event是其对应的common_timeout_list中最先超时的那个event,在该函数中,先用timeout变量保存了这个event的超时时间,由于这个超时时间是一个common_timeout,因此通过掩码按位与获得一个实际超时时间,接着是最关键的一步:把common_timeout_list中的timeout_event添加到事件监听中。而这个timeout_event,在event_base_init_common_timeout函数中就已经通过evtimer_assign(&new_ctl->timeout_event, base,common_timeout_callback, new_ctl);为其注册了回调函数等信息,需要注意的是,这里调用event_add_internal所使用的timeout并不是一个common_timeout,并且还是最先超时的那个event的超时时间。回到event_add_internal中,在刚才分析的event_queue_insert函数中,可以发现,如果这个event所设置的超时不是common_timeout,那么就会直接把这个超时放到min_heap中,简单来说,common_timeout_schedule就是把common_timeout_list中的最先超时的那个event的超时添加到了timeout_event中,而这个timeout_event,最终作为整个common_timeout_list的“代表”,被添加到了min_heap中。

        接下来再来说说,如果这个添加进去的“代表”timeout_event超时激活了,会发生什么。

激活common_timeout对应的event

       前面说过,被插入到heap中的timeout_event设置的回调函数是common_timeout_callback,当min_heap中的timeout_event发生超时而激活后,就会直接去调用common_timeout_callback,该函数定义如下:

static void
common_timeout_callback(evutil_socket_t fd, short what, void *arg)
{
	struct timeval now;
	struct common_timeout_list *ctl = arg;  //传入的参数是event所在的那个common_timeout_list
	struct event_base *base = ctl->base;
	struct event *ev = NULL;
	EVBASE_ACQUIRE_LOCK(base, th_base_lock);
	gettime(base, &now);  //获取系统时间
	while (1) {
		ev = TAILQ_FIRST(&ctl->events); //遍历这个common_timeout_list中的所有event,如果有超时的就添加到激活队列中
		if (!ev || ev->ev_timeout.tv_sec > now.tv_sec ||
		    (ev->ev_timeout.tv_sec == now.tv_sec &&
			(ev->ev_timeout.tv_usec&MICROSECONDS_MASK) > now.tv_usec))
			break;
		event_del_internal(ev);
		event_active_nolock(ev, EV_TIMEOUT, 1);
	}
	if (ev) //此时的ev如果不为空,那么它就是未来最先超时的那个event
		common_timeout_schedule(ctl, &now, ev); //重新将这个event的超时时间加上common_timeout_callback添加到min_heap中
	EVBASE_RELEASE_LOCK(base, th_base_lock);
}

      可以看到这个函数的功能实际上很简单,当作为common_timeout_list的timeout_event超时激活后,就会回到common_timeout_list中,从前往后遍历events链表中的event,如果发现超时的,就把超时的event以EV_TIMEOUT的形式激活。当遍历到第一个未超时的event,那么其后的所有event肯定都是未超时的了,然后就退出遍历。此时的ev指针就指向第一个未超时的event,这个event实际上就是未来最先超时的event,然后再次调用common_timeout_schedule函数,就以这个event的超时为基础,重新插入一个新的“代表”timeout_event到min_heap中。

       简单来说,common_timeout_callback函数的作用就是遍历common_timeout_list中的event,激活所有超时的event,并且根据未来最先超时的那个event重新设置一个新的“代表”timeout_event插入到min_heap中。

总结

      每一个base中都有一个common_timeout_queues,这是一个common_timeout_list的指针,每个元素都对应一个common_timeout_list。每一个这样的list中都放着具有相同超时时长的event,但是这些event按照超时时间升序排列,还有一个timeout_event,这个timeout_event也是一个event,只不过是内部使用的,因此它会有EVLIST_INTERNAL的标志,timeout_event的ev_timeout设置的超时时间是整个list中最先超时的那个event的超时时间,回调函数设置为common_timeout_callback,然后把timeout_event放到min_heap中。当timeout_event激活后,自动调用common_timeout_callback函数,该函数会去激活整个list中所有超时的event,并且删除这些超时的event,最后剩下来的第一个event就成了最先超时的那个event了,然后把它的超时时间重新设置到timeout_event中,再把timeout_event重新丢到min_heap中等待下一次超时。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

libevent源码学习(12):超时管理之common_timeout 的相关文章

  • 牛客网 - 华为OD算法机试(可内推)

    1 前言 这几天在闭关修炼数据结构和算法 也好几天没有更新博客了 其实我也没学多久的算法 满打满算牛客和leecode也就刷了四十来道题 其实算法也没有我们一开始想象的那么难 至少面试考的算法都还比较基础 今天参加了华为OD的机试 没有想象
  • Streamlit 讲解专栏(八):图像、音频与视频魔法

    文章目录 1 前言 2 st image 嵌入图像内容 2 1 图像展示与描述 2 2 调整图像尺寸 2 3 使用本地文件或URL 3 st audio 嵌入音频内容 3 1 播放音频文件 3 2 生成音频数据播放 4 st video 嵌
  • html5中链接和分组标签

    本小白今天新学了一点html5标签的用法 内容简单 希望和大家分享一下 对html5感兴趣的小白可以去CSDN上找视频教程具体学习 http edu csdn net course detail 489 5454 auto start 1
  • 进程的休眠与唤醒(等待队列)

    1 进程休眠 1 进程有三种基本状态 就绪态 阻塞态 运行态 lt 1 gt 阻塞态 进程缺少除了CPU之外的某些资源 因此该进程不能被运行 被阻塞住了不能被CPU调度 lt 2 gt 就绪态 进程分配到了除CPU之外的所有资源 等待CPU
  • GLSL里自定义attribute变量

    在做法线贴图的时候需要传入顶点的切线数据 这个顶点属性值是不包含在GLSL默认提供里的 需要自己实现 于是学习了一下 其实非常简单 首先需要glGetAttribLocation来获取变量的索引值 然后调用glVertexAttrib对其进
  • 微电子专业

    作者IC修真院 今天来聊聊微电子高校 提到微电子专业 肯定不得不说9所首批示范性微电子学院 今天就来盘他们 类似于专业设置 培养方案 课程设置这类信息 在学校官网上都是可以直接搜索到的 就不和大家赘述了 这里主要就方向优势 科研能力和业内认
  • PHP网站设计思路

    本文是对 PHP and MySQL Web Development 第5版27章中项目的总结 1 分析所需功能 列出主要功能模块 登录 注册 忘记密码 重设密码 登出 书签浏览 书签增加 书签删除 书签推荐 确定模块之间的先后转移关系 2
  • 鼠标滚动事件 - onmousewheel

    1 Jquery MouseWheel jquery默认是不支持支持鼠标滚轮事件 mousewheel jquery MouseWheel下载 https github com jquery jquery mousewheel blob m
  • 先后离开谷歌、雅虎后,梅姐的 AI 创业公司,再获两千万美金融资

    内容提要 硅谷一家创业公司 Lumi Labs 完成了近 2000 万美元的融资 这家鲜为人知的初创公司 凭什么能够拿下如此巨额的融资 这也许与其背后的大 boss 有关 它的创始人正是前雅虎 CEO 玛丽莎 梅耶尔 Marissa May
  • @JsonIgnoreProperties 解决实体中引用其他实体问题

    解决办法 json转换成的实体类加注解 JsonIgnoreProperties ignoreUnknown true 注意这是类级别的注解 JsonIgnore注解用来忽略某些字段 可以用在Field或者Getter方法上 用在Sette
  • _MSC_VER详细介绍

    MSC VER是微软的预编译控制 MSC VER可以分解为 MS Microsoft的简写 C MSC就是Microsoft的C编译器 VER Version的简写 MSC VER的意思就是 Microsoft的C编译器的版本 微软不同时期
  • 深度学习入门 ---- 张量(Tensor)

    文章目录 张量 张量在深度学习领域的定义 张量的基本属性 使用PyTorch 安装PyTorch 查看安装版本 创建张量 常用函数 四种创建张量的方式和区别 四则运算 张量 张量在深度学习领域的定义 张量 tensor 是多维数组 目的是把
  • 计算机配置内存容量怎么调,如何设置电脑虚拟内存,电脑虚拟内存设置多少最合理?...

    电脑虚拟内存是为了缓解CPU运行的压力而产生的一种新技术也可以理解为把电脑硬盘分出来一部分空间当作内存来使用 今天小编就为大家讲解下如何设置电脑虚拟内存 电脑虚拟内存设置多少最合理 希望对大家有所帮助 1 点击电脑左下角的开始菜单 找到控制
  • JavaScript中defer的作用

    JavaScript中defer的作用 Javascript中defer的作用是文档加载完毕了再执行脚本 这样会避免找不到对象的问题 defer是脚本程序强大功能中的一个 无名英雄 它告诉浏览器Script段包含了无需立即执行的代码 并且

随机推荐

  • Java浅拷贝和深拷贝

    Java Android 基础知识梳理 11 浅拷贝 Vs 深拷贝 Java对象数组深拷贝 java List复制 浅拷贝与深拷贝
  • C# 索引器(Indexer)

    C 索引器 Indexer 转自 http www runoob com csharp csharp indexer html 索引器 Indexer 允许一个对象可以像数组一样被索引 当您为类定义一个索引器时 该类的行为就会像一个 虚拟数
  • ibm服务器阵列卡与型号,IBM阵列卡介绍和服务器对阵列卡的支持情况

    ZDNetChina服务器站 芯片 组件配置技巧 目前IBM的阵列卡从控制的硬盘来说可以分成三大类 控制SCSI硬盘的SCSI RAID控制器 ServeRaid ServeRaid II ServeRaid 3L ServeRaid 3H
  • 【Leetcode】257. 二叉树的所有路径

    题目描述 题解 能用String解决的最好不要走StringBuilder 递归时注意空结点 null 回退和叶子结点判定回退 执行用时 9 ms 在所有 Java 提交中击败了30 66 的用户 内存消耗 39 1 MB 在所有 Java
  • Python 中的range(),arange()函数

    Python 中的range arange 函数 arange函数用于创建等差数组 使用频率非常高 arange非常类似range函数 会python的人肯定经常用range函数 比如在for循环中 几乎都用到了range 下面我们通过ra
  • 概率论中的重要不等式(Markov/Chebyshev/Jensen)

    1 Schwarz 不等式 对于任意的随机变量 和 均有 证明 假设 否则 有 所以不等式成立 我们有 即 2 Markov不等式 设随机变量 只取非负值 则对任意的 证明 固定正数 定义一个随机变量 易知 总成立 从而有
  • 1071svm函数 r语言_R语言机器学习之核心包e1071 - 数据分析

    R语言有很多包可以做机器学习 Machine Learning 的任务 机器学习的任务主要有有监督的学习方式和无监督的学习方式 有监督学习 在正确结果指导下的学习方式 若是正确结果是定性的 属于分类问题 若正确结果是定量的 属于回归问题 无
  • 软件设计模式(一)

    本章学习主要参照Datawhale开源学习及 大话设计模式 本项目结合 大话设计模式 这本书 总结了各种设计模式的基本概念 知识点和适用场景 先通过书中的案例 介绍了23种设计模式及其相关的代码示例 项目中有多种语言代码示例 本文主要采用P
  • Colorbox - a jQuery lightbox

    http www jacklmoore com colorbox http www jacklmoore com colorbox example5
  • C——结构体

    结构体 1 自定义类型 2 结构体 2 1 结构体类型说明 2 2 结构体变量的定义 2 3 结构体的初始化 2 4 结构体变量所占空间大小 2 5 结构体成员的引用 3 链表 3 1 处理动态链表所需的函数 3 2 指向自身的结构体类型
  • QScrollArea的简单使用

    当某个区域内的小部件尺寸超过了指定范围时 QScrollArea类提供了一个滚动区域 并生成滚动条 用于滚动显示区域内的所有小部件 关键代码 brief QScrollArea的简单使用 author xiaolei copyright v
  • stm32怎么用keil软件进行仿真?(必需掌握的技能)

    在做开发的前几年 基本上都没用仿真 有bug就尝试改程序 一边改一边调试 甚至都还不知道硬件仿真存在的价值 因为一直都没用过 而且很多芯片也不支持 直到有一次在做行车记录仪项目的时候 接触到了GRAIN公司的一款单片机 本来我打算是直接开干
  • 尽可能延后变量定义式的出现时间——条款26

    只要你定义了一个变量而其类型带有一个构造函数或析构函数 那么当程序控制流 control flow 到达这个变量定义式时 你便得承受构造成本 当这个变量离开其作用域时 你便得承受析构成本 即使这个变量最终未被使用 仍需耗费这些成本 所以你应
  • unity ARKit开发流程

    首先新的ARKit不再单独使用ARKit插件包进行开发 而是通过ARFoundation ARKit或者ARCore ARFoundation可以说是unity新出的针对AR方面开发的统一API接口 它将ARKit ARCore接口进行了统
  • Scikit学习-随机决策树

    Scikit学习 随机决策树 Scikit Learn Randomized Decision Trees This chapter will help you in understanding randomized decision tr
  • 虚拟机ubuntu安装samba服务

    安装samba apt get install samba 新建一个共享目录 mkdir home l work chmod 777 home l work 配置服务 配置 etc samba smb conf sudo smbpasswd
  • 2021-11-14

    艰难安装pyspider 环境 WIN10 python3 6 这个pyspider搞了我两天终于安装好了 1 首先把安装好的库全部删除 python pip freeze gt allpackages txt pip uninstall
  • yolov5训练l模型报错解决方案

    yolov5训练l模型报错解决方案 关于配置YOLOV5时出现错误 AttributeError Can t get attribute C3 on module models common from AttributeError Can
  • Git从入门到起飞(详细)

    Git从入门到起飞 Git从入门到起飞 什么是Git 使用git前提 注册git 下载Git 在Windows上安装Git 在macOS上安装Git 在Linux上安装Git 配置Git 配置全局用户信息 配置文本编辑器 创建第一个Git仓
  • libevent源码学习(12):超时管理之common_timeout

    目录 前言 common timeout的作用 common timeout的结构定义 common timeout与一般timeout的区分 获取common timeout在common timeout queues中的下标 判断一个t