在C++高并发场景,定时功能的实现有三大难题:高效、精准、原子性。
除了定时任务随时可能到期、而进程随时可能要退出之外,最近Workflow甚至为定时任务增加了取消功能,导致任务可能被框架调起之前被用户取消,或者创建之后不想执行直接删除等情况,而这些情况大部分来说都是由不同线程执行的,因此其中的并发处理可谓教科书级别! 那么就和大家一起看看Workflow在定时器的设计上做了哪些考虑,深扒细节,体验并发架构之美~
1. 高效的数据结构与timerfd
举个例子:实现一个server,收到请求之后,隔1s再回复给用户。
聪明的读者肯定知道,在server的执行函数中用**sleep(1)**是不行的,sleep()这个系统调用是会阻塞当前线程的,而异步编程里阻塞线程是高效的大忌!
所以我们可以使用timerfd,顾名思义就是用特定的fd来通知定时事件,把定时事件响应和网络事件响应都一起处理,用epoll管理就是一把梭。
现在离高效还差一点。回到例子,我们不可能每次收到一个请求都创建一个timerfd,因为高并发场景下一个server通常要抗上百万的QPS。
目前Workflow的超时算法做法是:一个poller有一个timerfd,内部利用了链表+红黑树的数据结构,时间复杂度在O(1)和O(logn)之间,其中n为poller线程的fd数量。
2. 精准的响应
这样的数据结构设计有什么好处呢?
-
写得快(放入一个新节点)
-
读得快(响应已超时的节点)
-
精度高(超时时间无精度损失)
Workflow源码在kernel和factory目录中都有对应的实现,kernel层是主要负责timerfd的地方,当前factory层还比较薄。我们重点看看上述数据结构。
写:由用户发起异步任务,将这个任务加到上述的链表+红黑树的数据结构中,如果这个超时是当前最小的超时时间,还会更新一下timerfd。
读:框架的网络线程每次会从epoll拿出事件,如果响应到超时事件,会把数据结构中已经超时的全部节点都拿出来,并调用任务的handle。
以下是从epoll处理超时事件的关键函数:
/*** poller响应timerfd的到时事件,并处理所有到时的定时任务 ***/
static void __poller_handle_timeout(const struct __poller_node *time_node, poller_t *poller)
{
...
// 锁里,把list与rbtree上时间已到的节点都从数据结构里删除,临时放到一个局部变量上
list_for_each_safe(pos, tmp, &poller->timeo_list)
{
...
node->removed = 1; // 标志位:【removed】
...
}
if (poller->tree_first)
{ ... }
// 锁外,设置state和error,并回调Task的handle()函数
while (!list_empty(&timeo_list