我是靠谱客的博主 和谐小土豆,最近开发中收集的这篇文章主要介绍服务器开发——定时器,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

学习新东西时,首先我们想了解下这个东西有什么作用,它能解决什么样的实际问题,带来什么样的好处。

                                                                                                    -----菜鸟语录

 

文章导读

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

 

最后

以上就是和谐小土豆为你收集整理的服务器开发——定时器的全部内容,希望文章能够帮你解决服务器开发——定时器所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(49)

评论列表共有 0 条评论

立即
投稿
返回
顶部