【定时器】4种定时器方式介绍及时间轮、时间堆实现

今天发现之前写的程序里的定时器类很有问题,这两天进行修改,今天先来复习一下理论知识。

服务器程序要处理种种定时任务,比如定期执行某回调函数监控客户连接等等。在并发的服务器中,显然不能用sleep这样的阻塞定时函数来做,以下介绍4种定时的方法,其中又以最后一种timerfd目前最为常见,至少我目前读到的muduo、handy都是用这一种。

1、socket选项

通过设置socket选项SO_RCVTIMED SO_SNDTIMED可以实现定时器,他们分别设置socket接受数据和发送数据的超时时间,此时再调用send、recv、accept等函数时就会触发超时。
如果超时,那么上述系统调用返回-1,并且将errno设置为EAGAIN或EWOULDBLOCK等。
程序(仅展示关键部分):

先设置超时时间
struct timeval timeout;
timeout.tv_sec = time;
timeout.tv_usec = 0;
socklen_t = sizeof(timeout);
配置socket为发送超时
setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMED, &timeout, len);
发起连接,使用connect系统调用。
ret = connect(sockfd, (struct sockaddr*)&address, sizeof(address));
如果返回-1且errorno为EINPROGRESS,说明超时时间到,进行处理。
if(ret == -1 && errno == EINPROGRESS)
{
	//process
}

2、SIGALRM信号

SIGALRM信号可以通过设置alarm函数来实现,每过alarm函数设置的定时周期T会生成SIGALRM信号,实现代码很长我就用文字叙述一下。

首先要定义一个定时器类,包含超时时间和任务回调函数两个重要成员,同时维护一个链表,链表每一个节点为一个定时器,且按照定时器到期时间从小到大排序。

例如:现在时间为0,alarm设置为每隔10秒发一次SIGALRM信号,那么设置三个定时器的超时时间分别为5,15,20,在第一次SIGALRM触发时,遍历链表,发现5小于10,则处理其回调函数并将这个定时器删除,下一个15大于当前时间10,那么遍历结束。下一次遍历将处理15和20,因为他们都小于等于当前时间20.

我们可以应用该方式来管理超时连接。

3、timerfd_系列函数

io复用的超时参数于传统的Reactor模式中使用,muduo中写到其定时精度只有毫秒级,而timerfd会更高,所以就不在这展开了。

timerfd是muduo采用的方式,多线程中处理信号是很麻烦的事情,而timerfd_create会创建一个文件描述符,当该文件在定时器超时那一刻会变为可读,那么epoll就能够很好的监听该定时器事件,用统一的方式来处理IO事件和超时事件。

4、时间轮和时间堆

之前提到的链表结构来放置定时器的效率较低,遍历一次时间复杂度为O(n),那么向其中添加和删除的最坏情况都是O(n),而时间轮实际上是利用了哈希的思想,将定时器散列在数组上,且每个数组中放的是链表,如下图:
【定时器】4种定时器方式介绍及时间轮、时间堆实现_第1张图片
每隔一定时间会使指针指向轮子上的一个格子,当此时有新连接需要添加定时器时就将其加入到这一格的链表中。

而在muduo中就是运用的这种方式,只不过更为巧妙,假设时间轮有8格,现在我们希望8s内都没有新数据的连接就属于非活动链接,将其断开掉。那么每隔1s转动一次指针,如果此时有新链接到来或者有连接产生数据交互,那么将该连接放置到这一格中,并将当前格子内的定时器全部弹出。

实际实现的时候,并不会把连接进行移动,而是采用引用计数的方式,用shared_ptr来进行管理,当连接收到数据就往当前的格子里放一个其对应的shared_ptr,指针计数加1,同时删除最后一个格子里所有shared_ptr,当某一个连接的shared_ptr计数为0时则会自动析构。

时间堆顾名思义就是用堆结构来进行定时器的储存,在c++11中优先级队列就是一个小根堆,可以使用它进行定时器的存储。

你可能感兴趣的:(Web)