1 概述
很多情况下我们需要在程序中定时去执行一些任务,比如定时发送心跳,定时计数,RPC定时超时唤醒等等,我们需要一个定时器来帮我们做这件事情,这个定时器需要按照我们所规定的时间定时唤醒去执行我们所赋给他的任务。那么对于一个定时器我们希望他准确且高效。
实现一个定时器主要有三个要素:唤醒装置、计时方式、元素存放的数据结构唤醒装置:不过无论使用什么方式进行定时唤醒,定时器都是存在误差的,这个误差来源于系统调用、代码逻辑等,也就是说一个定时器并不能真正做到精准的定时唤醒,不过只要这个误差在可接受范围内,那么我们默认他是正确的。通常定时器使用的唤醒装置有pselect、epoll_wait、condition_variable等等,这些都属于系统调用范畴,系统调用需要时间,pselect测试过系统调用本身在几十微秒的级别、epoll_wait应该也差不了多少,不过epoll_wait的输入时间是毫秒级别的,condition_variable和pselect输入级别可以达到纳秒级别,不过在用户态实现纳秒级定时器应该不可能。计时方式:定时器时间通常使用单调递增的时间,C++11中提供了std::steady_clock这种单调递增时间,配合C++11的条件变量很好用,通常不会使用墙上时钟作为定时器的计时,因为墙上时钟可以通过修改系统时间进行修改,如果修改了系统时间,会对定时器内的元素产生未定义的行为。 数据结构:实现一个定时器较常使用的数据结构是最小堆,当然可以使用链表、multimap等。针对应用场景不同可以实现不同的定时器策略,如果这个定时器所存放的元素较少,应用场景竞争压力不大,那么使用一个线程加一个最小堆,可以很方便的实现。
2 brpc定时器的实现原理
brpc是百度研发的开源rpc框架,性能甚佳,适合高并发高性能的开发场景,rpc框架必然需要使用定时器,因为一个RPC请求是有超时时间的,虽然RPC超时相对并不那么常发生。brpc内部使用bthread这种支持work stealing工作方式的协程,同时也支持pthread的方式,内部多线程并发处理用户请求。对于这样一个rpc框架,他所需要的定时器必须要满足:准确、低竞争高并发的要求。
brpc采用condition_variable的唤醒方式+墙上时钟+小顶堆的方式实现其定时器。在多线程场景中用户启动一个定时器通常通过对小顶堆加锁然后插入,这种多生产者单消费者的方式加锁解锁对于大并发的场景是非常敏感的,锁竞争较大的情况下抢锁操作很容易成为性能热点。通常降低竞争提高并发的方式可以通过做partition的方式,当然也可以引入无锁队列等方式,无锁队列并不是一定会比有锁队列快,而且无锁队列的正确性需要较为全面的测试来保证。
brpc通过partition的方式降低竞争,brpc内部为定时器单独启动一个线程Timerthread, Timerthread负责执行定时器唤醒并执行定时任务,为了避免多线程争抢,Timerthread内部将定时器元素拆分到多个bucket,每个bucket有自己的锁,bucket内部以链表的形式组织定时事件,虽然这还是一种多生产者单消费者模型,但是在总体上将插入定时事件的竞争降低到了1/bucket_num。定时事件通过调用Timerthread的schedule接口被插入,同时携带者其定时长度及回调函数,定时事件hash到对应bucket是以调用者的线程id与bucket的数量取余,大概率上可以达到均衡。
Timerthread的线程函数负责从这些bucket中取出,然后将定时器超时的事件逐个执行其回调函数。
brpc的定时器时间使用的是墙上时钟,这会存在问题,其开源的文档中也提到了这点。另外一点,所有超时的定时事件回调都在Timerthread中串行调用,如果回调过多,或者回调本身较为耗时,那么会影响后续定时事件被调度的时间。
3 brpc定时器源码
Timerthread:
class TimerThread {
public:
...
// 启动定时器线程
int start(const TimerThreadOptions* options);
// 等待定时器线程退出
void stop_and_join();
// 将fn, arg, asbtime封装成定时器事件push到对应的bucket
TaskId schedule(void (*fn)(void*), void* arg, const timespec& abstime);
...
private:
// 线程函数
void run();
// 当前定时器所有的buckets
Bucket* _buckets; // list of tasks to be run
internal::FastPthreadMutex _mutex; // protect _nearest_run_time
// _nearest_run_time代表当前定时器中最近的需要被调度的事件时间点
int64_t _nearest_run_time;
// 用于触发condition variable等待的信号
int _nsignals;
// pthread定时器自己的线程
pthread_t _thread; // all scheduled task will be run on this thread
};
Bucket
// bucket cache对齐,防止false sharing
class BAIDU_CACHELINE_ALIGNMENT TimerThread::Bucket {
public:
...
// 外围timerthread会调用bucket的schedule将事件插入其链表
// bucket并不为定时事件排序,也没必要排序,所有的排序都是在
// timerthread的线程函数中统一进行堆排序
ScheduleResult schedule(void (*fn)(void*), void* arg,
const timespec& abstime);
// timerthread的线程函数中会通过该函数获取当前bucket的所有事件
Task* consume_tasks();
private:
internal::FastPthreadMutex _mutex;
// 每个bucket有一个自己的记录,表示自己当前所存储的所有定时事件中最近要执行的时间点
int64_t _nearest_run_time;
// 链表头部
Task* _task_head;
};
线程函数
// timerthread的线程函数,负责从bucket取出定时事件并执行定时事件的回调
void TimerThread::run() {
run_worker_startfn();
int64_t last_sleep_time = butil::gettimeofday_us();
BT_VLOG << "Started TimerThread=" << pthread_self();
// 存放事件的容器,使用std::push/pop_heap对其进行堆排序
std::vector<Task*> tasks;
tasks.reserve(4096);
...
while (!_stop.load(butil::memory_order_relaxed)) {
...
// 从所有的bucket中取出定时事件
for (size_t i = 0; i < _options.num_buckets; ++i) {
Bucket& bucket = _buckets[i];
for (Task* p = bucket.consume_tasks(); p != nullptr; ++nscheduled) {
// p->next should be kept first
// in case of the deletion of Task p which is unscheduled
Task* next_task = p->next;
if (!p->try_delete()) { // remove the task if it's unscheduled
// 将定时事件push到容器然后排序
tasks.push_back(p);
// 进行堆排序,排序依据task_greater
std::push_heap(tasks.begin(), tasks.end(), task_greater);
}
p = next_task;
}
}
bool pull_again = false;
while (!tasks.empty()) {
...
// 将堆顶弹出
std::pop_heap(tasks.begin(), tasks.end(), task_greater);
tasks.pop_back();
// 执行堆顶的定时事件回调,这里所有的定时事件回调串行执行
if (task1->run_and_delete()) {
++ntriggered;
}
}
if (pull_again) {
BT_VLOG << "pull again, tasks=" << tasks.size();
continue;
}
...
timespec* ptimeout = NULL;
timespec next_timeout = { 0, 0 };
const int64_t now = butil::gettimeofday_us();
if (next_run_time != std::numeric_limits<int64_t>::max()) {
// 计算需要等待的下一个最小时间点
next_timeout = butil::microseconds_to_timespec(next_run_time - now);
ptimeout = &next_timeout;
}
busy_seconds += (now - last_sleep_time) / 1000000.0;
// 条件变量等待计算好的时间
futex_wait_private(&_nsignals, expected_nsignals, ptimeout);
last_sleep_time = butil::gettimeofday_us();
}
}