定时器优化
多路复用本质上为了多个任务可以把一个线程作为运行上下文,减少线程的创建,从而减少上下文的切换,有利提高性能。
全局容器
为了更好的使用多核的性能,我们将在每个核上设置一个全局的变量,然后使用散列方式将排队的定时器分发到响应的堆容器中。
/*我们使用一个宏来控制个数,这样很容易通过编译来控制数量*/
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;
}
解决并发
由于现在框架是多线程的,所以我们有两个并发问题,一是判断和设置所属事件循环描述符索引时;二是删除后,我们必须能判断回调函数是否还在执行,否则就不能安全的释放定时器的资源。
- 使用原子操作来设置判断分派索引,我们选择优化
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];
}
- 现在将
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
不需要原子的,因为他在索引设置成功后才会操作,而操作前又必须加锁。
-
为了保证定时器调度完成,我将使用一个等待队列来解决同步问题,这个等来队列的语义是内核给出的,但是可以用
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);
}
...
}
-
上面的一系列操作基本解决了线程安全的问题,只要我们以同步的方式删除定时器,就可以释放定时器持有的资源。而且我们分发到事件循环对象的实现中,只要已经分发,新的分发就会被忽略。这样我们很容易的实现分发到指定的事件循环中,实现定时器的串行化执行,这样多个定时器操作相同的资源就不用加锁保护了。
- 如果定时器被启动过,则同步删除定时器。
- 使用
__selete_loop()
来设置。 - 使用
timer->flags & INDEX_MASK
判断设置是否成功,如果失败再重复第一步。
所以我们应该提供最基础的接口,使用者可以组合出多样的功能,而不是方方面面的功能都提供一个接口。
提高精度
我们在添加定时器到事件循环中后,没有通知过时循环,整个定时器系统的状态发生了改变。那么如下的情况就会导致定时器延迟,如果现在队列定时器 A 在未来5毫秒超时,那么循环就会休眠5毫秒才会开始处理;现在另一个线程添加了一个更早的定时器 B 在未来 4 毫秒后超时。很显然 定时器 B 将延迟被调度。所以现在我们增加一种手段来通知循环,如果又更早超时的定时器排队就从休眠中醒来,重新计算超时,然后再休眠等待。