概述
学习新东西时,首先我们想了解下这个东西有什么作用,它能解决什么样的实际问题,带来什么样的好处。
-----菜鸟语录
文章导读
1.概念性的介绍下定时器,定时器的使用场景
2.定时器主流实现方式以及优缺点分析
3.定时器位于网络框架什么位置及原因
4.参考下muduo,libevent,以及nginx中的例子,并对比下几种实现的优点和缺点。并做小结。
一、定时器概述,应用场景
场景一:keep alive保活机制
成千上万个客户端去连接一台聊天服务器,那么就会存在成千上万个tcp连接。但是这些tcp连接是每时每刻都保持发包收包的活跃状态吗?不是!
某些tcp连接上,可能建立之后,在一天之内就没再发包/收包过,为了把有限的系统资源分配给更活跃的用户使用,我们应该设计一种方案来踢掉空闲连接。
场景二:游戏中,指定时间(或间隔时间)执行某种特定操作
1. 每日/每周/每月,特定时间执行一次操作
2. 循环执行的定时器,比如每隔一分钟刷新一次野怪
3.只执行一次的定时器,在60秒后定时器超期,执行操作A
定时器通常包含至少两个成员:一个超时时间(通常采用相对时间或者超时时间)和一个超时时间到达后的一个回调函数。有时候还可能包含回调函数被执行时需要传入的参数
通常定时器结构如下
class stTimer
{
public:
time_t expire;/*任务的超时时间,此处使用绝对时间*/
void* user_data;/*回调函数传入的参数*/
void (*cb_func)(void* arg);/*超时回调函数*/
};
二、定时器主流实现方式以及优缺点分析
定时器的种类,最小堆定时器
2.1 list + 线程超时检测
list<stTimer> timerList;
需要开启一个单独的线程来检测list容器中元素是否超时
void thread_func(void* arg)
{
stTimer* pTimer = (stTimer*)arg;
//遍历list容器中全部元素
//如果当前时间大于容器中的超时时间,则归为超时
if(currentTime > expire)
{
//调用超时回调函数cb_func
}
}
反思下这种方式的优点是什么?缺点是什么?
优点:代码实现简单,适合在定时器数量少(比如100以内)的场景,此时遍历list的开销比较小
缺点:如定时器数量太多,则效率很低。
2.2 timerfd+epoll IO复用函数
timerfd是Linux为用户程序提供的一个定时器接口。这个接口基于文件描述符,通过文件描述符的可读事件进行超时通知,因此可以配合select/poll/epoll等使用。也就是说当定时器超时的时刻,会触发该timerfd文件描述符的可读事件,而可读事件可以和网络模型相结合来配套使用。
2.2.1 timerfd相关api函数介绍
#include <sys/timerfd.h>
int timerfd_create(int clockid, int flags)
timerfd_create()函数创建一个定时器对象,同时返回一个与之关联的文件描述符。
int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);
此函数用于设置新的超时时间,并开始计时,能够启动和停止定时器;
int timerfd_gettime(int fd, struct itimerspec *curr_value);
timerfd_gettime()函数获取距离下次超时剩余的时间
示例代码如下
#include <sys/epoll.h>
#include <sys/timerfd.h>
#include <time.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define MX_EVNTS 10
#define EPL_TOUT 3000
#define MX_CNT 5
struct param{
struct itimerspec its;
int tfd;
};
void *strt_eplth(void *arg)
{
struct epoll_event evnts[MX_EVNTS];
int *eplfd = (int *)arg;
int n = -1;
size_t i,cnt = 0;
while(1){
n = epoll_wait(*eplfd,evnts,MX_EVNTS,EPL_TOUT);
if(n == -1){
perror("epoll_wait() error");
break;
}else if(n == 0){
printf("time out %d sec expiredn",EPL_TOUT / 1000);
break;
}
for(i = 0; i < n;i++){
struct param *pm = (struct param *)(evnts[i].data.ptr);
printf("tfd: %dninitial expiration: %ldninterval: %ldnn",
pm->tfd,
(long)(pm->its.it_value.tv_sec),
(long)(pm->its.it_interval.tv_sec));
if(epoll_ctl(*eplfd,EPOLL_CTL_DEL,pm->tfd,NULL) != 0){
perror("epoll_ctl(DEL) error in thread");
break;
}
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
pm->its.it_value.tv_sec =
pm->its.it_value.tv_sec +
pm->its.it_interval.tv_sec;
ev.data.ptr = pm;
if(timerfd_settime(pm->tfd,TFD_TIMER_ABSTIME,&(pm->its),NULL) != 0){
perror("timerfd_settime() error in thread");
break;
}
if(epoll_ctl(*eplfd,EPOLL_CTL_ADD,pm->tfd,&ev) != 0){
perror("epoll_ctl(ADD) error in thread");
break;
}
}
if(++cnt == MX_CNT){
printf("cnt reached MX_CNT, %dn",MX_CNT);
break;
}
}
close(*eplfd);
pthread_exit(NULL);
}
int create_timerfd(struct itimerspec *its,time_t interval)
{
int tfd = timerfd_create(CLOCK_MONOTONIC,TFD_NONBLOCK);
if(tfd < 0){
perror("timerfd_create() error");
return -2;
}
struct timespec nw;
if(clock_gettime(CLOCK_MONOTONIC,&nw) != 0){
perror("clock_gettime() error");
return -1;
}
its->it_value.tv_sec = nw.tv_sec + interval;
its->it_value.tv_nsec = 0;
its->it_interval.tv_sec = interval;
its->it_interval.tv_nsec = 0;
return tfd;
}
int main()
{
time_t interval = 2;
struct itimerspec its;
struct timespec nw;
//创建timerfd定时器
int tfd = timerfd_create(CLOCK_MONOTONIC,TFD_NONBLOCK);
if(tfd < 0){
perror("timerfd_create() error");
return -1;
}
if(clock_gettime(CLOCK_MONOTONIC,&nw) != 0){
perror("clock_gettime() error");
return -1;
}
//设置超时时间或间隔时间
its->it_value.tv_sec = nw.tv_sec + interval;
its->it_value.tv_nsec = 0;
its->it_interval.tv_sec = interval;
its->it_interval.tv_nsec = 0;
//创建epoll
int eplfd = epoll_create1(0);
if(eplfd < 0){
perror("epoll_create1() error");
return -1;
}
struct param pm;
pm.its = its;
pm.tfd = tfd;
if(timerfd_settime(pm.tfd,TFD_TIMER_ABSTIME,&(pm.its),NULL) != 0){
perror("timerfd_settime() error");
return -1;
}
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.ptr = ±
//将timerfd加入到epoll中
if(epoll_ctl(eplfd,EPOLL_CTL_ADD,pm.tfd,&ev) != 0){
perror("epoll_ctl() error");
return -1;
}
pthread_t pid;
if(pthread_create(&pid,NULL,strt_eplth,(void *)&eplfd) != 0){
perror("pthread_create() error");
return -1;
}
/*
add another timerfd.
INTERVAL = 1;
struct itimerspec its2;
int tfd2 = create_timerfd(&its2,INTERVAL);
if(tfd2 < 0)
return -1;
struct param pm2;
pm2.its = its2;
pm2.tfd = tfd2;
if(timerfd_settime(pm2.tfd,TFD_TIMER_ABSTIME,&(pm2.its),NULL) != 0){
perror("timerfd_settime() error");
return -1;
}
struct epoll_event ev2;
ev2.events = EPOLLIN | EPOLLET;
ev2.data.ptr = &pm2;
if(epoll_ctl(eplfd,EPOLL_CTL_ADD,pm2.tfd,&ev2) != 0){
perror("epoll_ctl() error");
return -1;
}
*/
if(pthread_join(pid,NULL) != 0){
perror("pthread_join() error");
return -1;
}
close(tfd);
//close(tfd2);
return 0;
}
下面我们来思考下,采用timerfd+epoll的方式定时器有什么优缺点?
优点:
1.时间精度高
2. 创建定时任务和周期性任务,然后扔到epoll中让内核帮你侦听文件描述符可读事件。
使得定时器操作能和epoll模型很好的结合起来。
缺点:
1.程序的总体逻辑有点乱
2.超时回调函数中不宜做较长时间操作的事情,比如数据库请求,大量写文件操作等等。
3.每个timerfd就是一个文件描述符,文件描述符属于系统资源,如果定时器的数量级是在千万级的,那么会非常浪费系统资源,这点要格外注意,凡事有利有弊。
2.3 最小时间堆
首先心里默念三遍,最小时间堆是什么?最小时间堆很简单,然后你就学会了,哈哈
堆是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于(或不小于)其左子节点和右子节点的值。
最大堆和最小堆是二叉堆的两种形式。
最大堆:根结点的键值是所有堆结点键值中最大者。
最小堆:根结点的键值是所有堆结点键值中最小者。
最小堆长什么样子?
最小堆的算法能够保证堆首的元素是堆中最小的元素。如上图所示,3是堆中最小元素。
最小堆的初始化、插入、删除操作等等这些在讲解基础算法的书里面都有,感兴趣的小伙伴可以自己实现下
如果是在工程上使用,我们直接可以使用C++标准库中的优先级队列来
std::priority_queue
优先级队列有两种常用的声明方式
std::priority_queue<T> pq;
std::priority_queue<T, std::vector<T>, cmp> pq;
优先级队列可以自定义cmp仿函数,来实现最大堆和最小堆,优先级队列默认是最大堆
优点:
1.适合少量定时器
2.代码实现简单
缺点:
1.如定时器数量太多,则效率很低。
2.4 时间轮
当初我在学习时间轮时,心里一直在想,为什么会出现时间轮这个概念?使用时间轮相比于其他定时器的优势和好处是什么?
网络服务端程序是需要短时间内接收大量客户端连接,俗称“高并发”,同时还需要管理这些连接,负责不同连接之间的数据转发等。
1) 如果一个客户端连接在连续8秒钟(也可自定义时间间隔)内没有收到数据,那么可以在一定程度上认为这是个无效连接,应该将其断开。
2)如果一个客户端连接在连续7秒钟内没有收到数据,但是在第8秒(马上触发超时)连接上有数据收发,那么此时要更新此连接对应定时器剩余时间为8秒,而不是1秒。
3) 服务端管理着多达数万到数十万不等的连接数,因此我们没法为每个连接使用一个Timer,那样太消耗资源不现实。
于是我搜集了下牛人前辈的论文,发现《Hashed and hierarchical timing wheels: efficient data structures for implementing a timer facility》这篇论文详细比较了实现定时器的各种数据结构,并提出了层次化的 timing wheel 与 hash timing wheel 等新结构。针对本文要解决的问题的特点,我们不需要实现一个通用的定时器,只用实现 simple timing wheel 即可。
时间轮有两种实现方式
1.智能指针 + 环形队列(muduo采用的方式)
智能指针采用的是shared_ptr和weak_ptr
shared_ptr<tcpConnection> tcpConnPtr;
shared_ptr是基于引用计数的智能指针,用于共享对象的所有权,即多个指针指向同一个对象。
先看简单看个例子理解下智能指针的概念和使用
#include <iostream>
#include <memory>
using namespace std;
class Example
{
public:
Example() : e(1) { cout << "Example Constructor..." << endl; }
~Example() { cout << "Example Destructor..." << endl; }
int e;
};
int main() {
shared_ptr<Example> pInt(new Example());
cout << (*pInt).e << endl;
cout << "pInt引用计数: " << pInt.use_count() << endl;
shared_ptr<Example> pInt2 = pInt;
cout << "pInt引用计数: " << pInt.use_count() << endl;
cout << "pInt2引用计数: " << pInt2.use_count() << endl;
}
输出结果如下:
Example Constructor...
pInt: 1
pInt引用计数: 1
pInt引用计数: 2
pInt2引用计数: 2
Example Destructor...
对比我们上面的代码可以看到:当我们将一个指向Example对象的指针交给pInt管理后,其关联的引用计数为1。接下来,我们用pInt初始化pInt2,两者关联的引用计数值增加为2。随后,函数结束,pInt和PInt2相继离开函数作用于,相应的引用计数值分别自减1最后变为0,于是Example对象被自动释放(调用其析构函数)。
8 个桶组成的循环队列。
第一个桶放下一秒将要超时的连接,
第二个放下 2 秒将要超时的连接。
每个连接一收到数据就把自己放到第 8 个桶(环形缓冲区的尾部),
然后在每秒钟的 callback 里把第一个桶里的所有连接断开,把这个空桶挪到队尾。
这样大致可以做到 8 秒钟没有数据就超时断开连接。
更重要的是,每次不用检查全部的 connection,只要检查第一个桶里的 connections,相当于把任务分散了。
如下图所示:
1.连接超时被断开的情况
假设在某个时刻,conn 1 到达,把它放到当前格子中,它的剩余寿命是 7 秒。此后 conn 1 上没有收到数据。
1 秒钟之后,tail 指向下一个格子,conn 1 的剩余寿命是 6 秒。
又过了几秒钟,tail 指向 conn 1 之前的那个格子,conn 1 即将被断开。
下一秒,tail 重新指向 conn 1 原来所在的格子,清空其中的数据,断开 conn 1 连接。
如果不止一个连接的话,就把环形缓冲区的元素改为set集合类型,类比成一个时间圆盘下面挂着很多链子。
每次时间轮超时检测时,检测当前指针所在的set中元素的引用计数是否为0,为0则超时。
2.普通环形队列 + map<连接,槽位>表(java版本的实现)
以本文开头所述的管理大量连接 timeout 的场景为例,描述一下 timing wheel的具体实现细节。
定时轮的工作原理可以类比于始终,如上图箭头(指针)按某一个方向按固定频率轮动,每一次跳动称为一个 tick。
这样可以看出定时轮由个3个重要的属性参数,ticksPerWheel(一轮的tick数),tickDuration(一个tick的持续时间)
以及 timeUnit(时间单位),例如 当ticksPerWheel=60,tickDuration=1,timeUnit=秒,这就和现实中的始终的秒针走动完全类似了。
三、定时器位于网络框架什么位置
四、libevent,nginx库定时器对比
4.1 libevent库
libevent采用最小堆来管理大量定时器
event_base_dispatch->event_base_loop->timeout_process
在timeout_process函数中检测哪些定时器已经超时
/* Activate every event whose timeout has elapsed. */
static void
timeout_process(struct event_base *base)
{
/* Caller must hold lock. */
struct timeval now;
struct event *ev;
//最小堆是否为null
if (min_heap_empty_(&base->timeheap)) {
return;
}
//获取当前时间
gettime(base, &now);
//获取最小堆的首元素,此元素超时时间间隔最小,将最先触发超时
while ((ev = min_heap_top_(&base->timeheap))) {
if (evutil_timercmp(&ev->ev_timeout, &now, >))
break;
/* delete this event from the I/O queues */
event_del_nolock_(ev, EVENT_DEL_NOBLOCK);
event_debug(("timeout_process: event: %p, call %p",
ev, ev->ev_callback));
event_active_nolock_(ev, EV_TIMEOUT, 1);
}
}
4.2 nginx定时器
nginx定时器采用的是红黑树管理的。
源代码分析
nginx的定时器的源代码是在ngx_event_timer.c文件中
ngx_event_timer_init -> 定时器红黑树的初始化
ngx_event_expire_timers->寻找是否有超时的定时器
ngx_event_expire_timers被ngx_process_events_and_timers来调用
ngx_process_events_and_timers在ngx_single_process_cycle函数中,使用for循环不断的检测是否有超时事件被触发。
for ( ;; ) {
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "worker cycle");
ngx_process_events_and_timers(cycle);
if (ngx_terminate || ngx_quit) {
for (i = 0; cycle->modules[i]; i++) {
if (cycle->modules[i]->exit_process) {
cycle->modules[i]->exit_process(cycle);
}
}
ngx_master_process_exit(cycle);
}
if (ngx_reconfigure) {
ngx_reconfigure = 0;
ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reconfiguring");
cycle = ngx_init_cycle(cycle);
if (cycle == NULL) {
cycle = (ngx_cycle_t *) ngx_cycle;
continue;
}
ngx_cycle = cycle;
}
if (ngx_reopen) {
ngx_reopen = 0;
ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reopening logs");
ngx_reopen_files(cycle, (ngx_uid_t) -1);
}
}
ngx_process_events_and_timers(cycle);
if (ngx_terminate || ngx_quit) {
for (i = 0; cycle->modules[i]; i++) {
if (cycle->modules[i]->exit_process) {
cycle->modules[i]->exit_process(cycle);
}
}
ngx_master_process_exit(cycle);
}
if (ngx_reconfigure) {
ngx_reconfigure = 0;
ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reconfiguring");
cycle = ngx_init_cycle(cycle);
if (cycle == NULL) {
cycle = (ngx_cycle_t *) ngx_cycle;
continue;
}
ngx_cycle = cycle;
}
if (ngx_reopen) {
ngx_reopen = 0;
ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reopening logs");
ngx_reopen_files(cycle, (ngx_uid_t) -1);
}
}
采用红黑树管理定时器的好处是什么?为什么nginx没用采用最小堆来使用?
待完成
参考链接
https://blog.csdn.net/haolipengzhanshen/article/details/52012096
https://blog.csdn.net/hrn1216/article/details/51465270
http://www.cppblog.com/Solstice/
知乎上关于定时器的讨论
https://www.zhihu.com/question/32251997/answer/56320496
最小堆的插入、删除图解
https://blog.csdn.net/u011068702/article/details/52712634
最后
以上就是和谐小土豆为你收集整理的服务器开发——定时器的全部内容,希望文章能够帮你解决服务器开发——定时器所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复