最小堆定时器 —— 优化

定时器优化

多路复用本质上为了多个任务可以把一个线程作为运行上下文,减少线程的创建,从而减少上下文的切换,有利提高性能。

全局容器

为了更好的使用多核的性能,我们将在每个核上设置一个全局的变量,然后使用散列方式将排队的定时器分发到响应的堆容器中。

/*我们使用一个宏来控制个数,这样很容易通过编译来控制数量*/
struct ev_loop ev_loops[NR_CPU];

现在需要一个分发的辅助函数,用于选择一个描述符。

/*我直接使用指针用作hash*/
struct ev_loop *select_loop(struct ev_timer *timer)
{

	/*现在可以使用指针计算一个散列值,假设我们是32位的系统*/
	long hash = (long)timer * 0x61C88647UL;
	return &ev_loops[hash % NR_CPU];
}

在 事件循环 描述符中添加一些字段来记录关联的线程。

struct ev_loop {
	...
	int index; /*全局的索引*/
	pthread_t thread;
};

使用一个新的函数来启动事件系统,并未每个事件循环描述符创建一个线程,让他来运行事件循环函数就可以了。

void *event_loop_wrap(void *arg)
{
	event_loop(arg);
}

void ev_startup(void)
{
	/*初始化其他字段,略*/
	...
	/*为每个时间循环创建线程*/
	for (int i = 0; i < NR_CPUS; i++) {
		ev_loops[i].index = i;
		pthread_create(&ev_loops[i].thread, 0, event_loop_wrap, &ev_loops[i]);
	}
}

定时器修改

现在我们改变 ev_timer_modify() 少传递一个参数。在这之前我们修改一下定时器描述符的字段,使用一个 整型 来做状态机,内核常用的手段,就是将一个 整型 的多个位拆开来使用。

struct ev_timer {
	unsigned long index:8;
	unsigned long pending:1;
	ev_timer_fn func;
	struct timerqueue_node node;
};

使用8位来存储所在事件循环对象的索引,1位的置位与否来判断是否已排队(我们假设都是小端计算机)。

int ev_timer_modify(struct ev_timer *timer, uint32_t expires)
{
	int ret = 0;
	struct ev_loop *loop;

	/*并发问题后面考虑*/
	if (timer->index != 0xff) {
		loop = &ev_loops[timer->index];	
	} else {
		/*但是输入固定,这个函数输出固定*/
		loop = select_loop(timer);
		timer->index = (int)(ev_loops - loop);
	}

	/*已排队,从容器中删除*/
	if (timer->pending) {
		timer->pending = 0;
		timerqueue_del(&loop->timer_queue, &timer->node);
		ret = 1;
	}

	/*更新各个字段*/
	timer->pending = 1;
	timer->node.expires = get_timestamp() + expires;
	...	
	/*排队到定时器堆*/
	timerqueue_add(&loop->timer_queue, &timer->node);
	return ret;
}

我们仅仅为了讲解优化,你可以保留这个接口,方便使用自己创建的事件循环对象来作为载体。

定时器删除

现在的优化思路是,定时器一定,那么会将被分派到固定的事件循环中,那么删除时,如果还没有分派过,则一定没有排队。

int ev_timer_delete(struct ev_timer *timer)
{
	struct ev_loop *loop;

	if (timer->index == 0xff || !timer->pending)
		return 0;

	loop = &ev_loops[timer->index];	
	...
	return 1;
}

解决并发

由于现在框架是多线程的,所以我们有两个并发问题,一是判断和设置所属事件循环描述符索引时;二是删除后,我们必须能判断回调函数是否还在执行,否则就不能安全的释放定时器的资源。

  1. 使用原子操作来设置判断分派索引,我们选择优化 select_loop() 来实现,由于我们要使用原子操作,位域就不满足条件了,我使用位操作。
#define INDEX_MASK 0xffUL
#define PENDING_MASK 0x100UL
#define PENDING_BIT PENGDING_MASK

struct ev_timer {
	unsigned long flags; /*8bits:index,1bit:pending*/
	...
};

unsigned long hash_timer(struct timer *timer)
{
	long hash = (long)timer * 0x61C88647UL;
	return hash % NR_CPU;
}
/*
 * 以参数的方式传入 希望设置的索引 方便扩展
 */
struct ev_loop *__select_loop(struct timer *timer, unsigned long hint)
{
	unsigned long index;
	unsigned long flags;

	/*将状态原子读取到栈中*/
again:
	flags = READ_ONCE(timer->flags);
	index = flags & INDEX_MASK;
	/*判断是否已设置*/
	if (index == INDEX_MASK) {
		/*原子比较交换*/
		bool rc = cmpxchg(&timer->flags, flags, flags|hint);
		if (!rc)
			goto again;
		index = hint;
	}

	/*现在已确切了索引*/
	return &ev_loops[index];
}

struct ev_loop *select_loop(struct timer *timer)
{
	unsigned long index = READ_ONCE(timer->flags) & INDEX_MASK;
	return index == INDEX_MASK ?
		__select_loop(timer, hash_timer(timer)) : &ev_loops[index];
}
  1. 现在将 ev_timer_modify() 加上锁操作。
int ev_timer_modify(struct ev_timer *timer, uint32_t expires)
{
	int ret = 0;
	struct ev_loop *loop = select_loop(timer);	

	/*我们使用互斥锁来*/
	mutex_lock(&loop->lock);
	if (timer->flags & PENDING_MASK) {
		ret = 1;
		timer->flags |= PENDING_BIT;
		/*删除*/
	}
	/*重新排队*/
	mutex_unlock(&loop->lock);

	return ret;
}

我们操作 pengding 不需要原子的,因为他在索引设置成功后才会操作,而操作前又必须加锁。

  1. 为了保证定时器调度完成,我将使用一个等待队列来解决同步问题,这个等来队列的语义是内核给出的,但是可以用 futex 来实现,我将在其他文章介绍。为了便于理解,我们大致介绍一下语义:

    • 定义一个等待队列,由于排队需要唤醒的进程(或线程)。
    • 将需要查看条件的进程加入等待队列,然后查看,如果条件不满足就休眠等待。
    • 将条件设置为真的进程在改变条件后,遍历等待队列,将等待的进程唤醒。

我们在事件循环对象中加入一个等待队列字段。

struct ev_loop {
	wait_queue_head_t wait_queue;
	...
};

优化删除定时器,加上一个参数,表示是否同步等待调度完成。

int ev_timer_delete(struct ev_timer *timer, bool sync)
{
	int ret = 0;
	struct ev_loop *loop;

	/*如果不是同步删除的,我们不需加锁来简单的查看是否排队*/
	if (!sync && !(timer->flags & PENDING_MASK))
		return 0;

	loop = select_loop(timer);	

	mutex_lock(&loop->lock);
	/*加锁再次判断*/
	if (timer->flags & PENDING_MASK) {
		ret = 1;
again:
		timer->flags &= ~PENDING_MASK;
		/*从容器中删除*/
		...
	}

	/*现在我们查看是否timer正在被调度*/
	if (sync) {
		DEFINE_WAITQUEUE(wait);	
		add_wait_queue_locked(&loop->wait_queue, &wait);
		/*如果当前定时器正在被调度定时器,则等待*/
		while (loop->current == timer) {
			mutex_unlock(&loop->lock);
			/*记得解锁等待*/
			wait_on(&wait);
			mutex_lock(&loop->lock);
		}
		remove_wait_queue_locked(&loop->wait_queue, &wait);
		/*因为有解锁,我们必须再次判断是否在调度后,又排队了的情况*/
		if (timer->flags & PENDING_MASK)
			goto again;
	}

	mutex_unlock(&loop->lock);

	return ret;
}

在事件循环中加入互斥操作。

void event_loop(struct ev_loop *loop)
{
	...
	mutex_lock(&loop->lock);
	do {
		/*计算应该休眠多久*/
		usecs = calc_timeout(loop);

		/*休眠*/
		mutex_unlock(&loop->lock);
		usleep_unintr(usecs);
		mutex_lock(&loop->lock);

		/*进程被唤醒,检查超时的定时器并处理*/
		process_timer(loop);
	} while (loop->running);
	mutex_unlock(&loop->lock);
}

在定时器处理函数中加入唤醒操作。

void process_timer(struct ev_loop *loop)
{
	...
	while ((next = timerqueue_getnext(&loop->timer_queue))) {
		timer = container_of(next, struct ev_timer, node);
		/*比较时间戳,使用绝对时间的好处,就是直接比较,不用转换*/
		if (now < timer->expires)
			break;

		/*从队列中删除*/
		timerqueue_del(&loop->timer_queue, &timer->node);

		/*开始处理*/	
		loop->current = timer;
		mutex_unlock(&loop->lock);

		timer->func(timer);

		mutex_lock(&loop->lock);
		loop->current = NULL;
		/*查看是否有等待唤醒的等待者*/
		if (waitqueue_active(&loop->wait_queue))
			wake_up_all_locked(&loop->wait_queue);
	}
	...
}
  1. 上面的一系列操作基本解决了线程安全的问题,只要我们以同步的方式删除定时器,就可以释放定时器持有的资源。而且我们分发到事件循环对象的实现中,只要已经分发,新的分发就会被忽略。这样我们很容易的实现分发到指定的事件循环中,实现定时器的串行化执行,这样多个定时器操作相同的资源就不用加锁保护了。

    • 如果定时器被启动过,则同步删除定时器。
    • 使用 __selete_loop() 来设置。
    • 使用 timer->flags & INDEX_MASK 判断设置是否成功,如果失败再重复第一步。

所以我们应该提供最基础的接口,使用者可以组合出多样的功能,而不是方方面面的功能都提供一个接口。

提高精度

我们在添加定时器到事件循环中后,没有通知过时循环,整个定时器系统的状态发生了改变。那么如下的情况就会导致定时器延迟,如果现在队列定时器 A 在未来5毫秒超时,那么循环就会休眠5毫秒才会开始处理;现在另一个线程添加了一个更早的定时器 B 在未来 4 毫秒后超时。很显然 定时器 B 将延迟被调度。所以现在我们增加一种手段来通知循环,如果又更早超时的定时器排队就从休眠中醒来,重新计算超时,然后再休眠等待。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值