定时任务的处理方式
服务端的驱动逻辑主要涉及两类事件的处理:网络I/O事件和定时事件,不同的框架采用不同的方式来整合这两种事件的处理流程:
- 在同一个线程中先处理I/O事件,再处理定时事件,如nginx、redis。利用 epoll_wait 监测I/O的同时依靠 timeout 参数来实现所谓定时,缺点是I/O的处理导致延时比较严重。
- 网络事件和I/O事件在不同线程中处理,如skynet。由单独的线程更新检测定时器,并把定时事件发送到消息队列中。
实现方案
首先需要为定时器选择合适的数据结构,其应该满足两点基本要求:
- 节点元素有序(越近要触发的任务越靠前)
- 可快速查找到key最小的节点
比较合适的数据结构包括红黑树、最小堆以及时间轮。
1)红黑树
增删改的时间复杂度都为O(logn),获取最小节点的复杂度也为O(h),h是树的高度。
Nginx的定时器就是采用红黑树实现,并且通过信号打断epoll_wait的方式来解决定时误差大的问题。
2)最小堆
增、删和改的时间复杂度为O(logn);获取最小节点的复杂度为O(1)。因为堆不是完全有序的,所以效率相对于红黑树还是会高一点的,复杂度也比较稳定。
libevent 的定时器便是采用最小堆来实现。
3)时间轮
时间轮实质上采用循环数组实现,描述了钟表的运行方式,在某个时间节点执行注册到该节点下