定时是指在一段时间之后触发某段代码的机制,我们可以在这段代码中依次处理所有到期的定时器。在对定时事件管理时,要将定时事件封装成定时器。通过使用某种容器类数据结构(链表、时间轮)将所有定时器串联起来,实现对定时事件的统一管理。
Linux提供了3中定时方法,分别是:
- socket选项SO_RCVTIMEO和SO_SNDTIMEO
- SIGALRM信号
- I/O复用系统调用的超时函数
11.1 socket选项SO_RCVTIMEO和SO_SNDTIMEO
SO_RCVTIMEO:接收数据超时
SO_SNDTIMEO:发送数据超时
仅对数据接收和发送相关的socket专用系统有效,这些系统调用包括send、sendmsg、recv、recvmsg、accept、connect。效果如下表:
由上表可见,根据系统调用的返回值及errno来判断超时时间是否已到,从而决定是否开始处理定时任务。下面是connect使用SO_SNDTIMEO来定时的用例:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
//超时连接函数
int timeoutConn(const char* ip, int port, int time)
{
int ret = 0;
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
inet_pton(AF_INET, ip, &addr.sin_addr);
addr.sin_port = htons(port);
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
assert(sockfd >= 0);
/*通过选项SO_SNDTIMEO和SO_RCVTIMEO所设置的超时时间的类型是timeval,
这和select系统调用的超时参数类型相同
*/
struct timeval timeout;
timeout.tv_sec = time;
timeout.tv_usec = 0;
socklen_t len = sizeof(timeout);
//设置发送超时时间
ret = setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len);
assert(ret != -1);
ret = connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
if (ret == -1)
{
/*超时对应的错误号是EINPROGRESS.下面这个条件如果成立,
我们就可以处理定时任务了
*/
if (errno == EINPROGRESS)
{
printf("connecting timeout, process timeout logic\n");
return -1;
}
printf("errno occur when connect to server\n");
return -1;
}
return sockfd;
}
int main()
{
int ret = 0;
const char* ip = "127.0.0.1";
int port = 12345;
int sockfd = timeoutConn(ip, port, 5);
if (sockfd < 0)
{
return 1;
}
return 0;
}
11.2 SIGALRM信号
由alarm和setitimer函数设置的实时闹钟一旦超时,将触发SIGALRM信号。因此,可以利用该信号处理函数来处理定时任务。
下面介绍如何使用SIGALRM信号定时。
11.2.1 基于升序链表的定时器
下面使用代码实现一个简单的升序定时器链表,即:将定时器作为节点链接起来,将启动的定时器按照超时时间做升序排序。sort_timer_lst是一个升序链表。其核心函数tick相当于一个心跳函数,每隔一段固定的时间就执行一次,以检测并处理到期的任务。判断定时任务到期的依据时定时器的expire值小于当前的系统时间。添加定时器的时间复杂度是O(n),删除定时器的时间复杂度是O(1),执行定时任务的时间复杂度是O(1)。
为了便于阅读,实现放在头文件LST_TIMER.h中。
#ifndef LST_TIMER
#define LST_TIMER
#include <time.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define BUFFER_SIZE 64
class UtilTimer; //前向声明
//用户数据结构:客户端socket地址、socket描述符、读缓存和定时器
struct ClientData
{
struct sockaddr_in address;
int sockfd;
char buf[BUFFER_SIZE];
UtilTimer* timer;
};
//定时器类
class UtilTimer
{
public:
//初始化节点的前向指针、后向指针
UtilTimer(): prev(NULL), next(NULL){};
public:
time_t expire; //任务超时时间,这里使用绝对时间
void (*cbFunc)(ClientData*); //任务回调函数
//回调函数处理的客户数据,由定时器的执行者传递给回调函数
ClientData* userData;
UtilTimer* prev; //指向前一个定时器
UtilTimer* next; //指向下一个定时器
};
//定时器链表,升序、双向、带有头节点、尾节点
class SortTimerList
{
public:
SortTimerList() : head(NULL), tail(NULL) {};
//链表被删除时,删除其中所有的定时器
~SortTimerList()
{
UtilTimer* tmp = head;
while(tmp)
{
head = tmp->next;
delete tmp;
tmp = head;
}
}
//将timer添加到链表中
void addTimer(UtilTimer* timer)
{
if (!timer)
{
return;
}
if (!head) //如果head为空,则将当前timer作为头、尾节点
{
head = tail = timer;
return;
}
/*
如果timer的超时时间小于当前链表中所有定时器的超时时间,
则把该定时器插入链表头部,作为链表的头节点,否则需要调用
重载函数addTimer(UtilTimer* timer, UtilTimer* lsthead),把它
插入链表中合适的位置,保证链表的升序
*/
if (timer->expire < head->expire) //当成头节点插入
{
timer->next = head;
head->prev = timer;
head = timer;
return;
}
addTimer(timer, head);
}
/*
当某个定时任务发生变化,调整对应的定时器在链表中的位置。
该函数只考虑被调整的定时器的超时时间延长的情况
*/
void addjustTimer(UtilTimer* timer)
{
if (!timer)
{
return;
}
UtilTimer* tmp = timer->next;
/*
如果被调整的timer处在链表尾部,或者该定时器新的超时值仍小于其下一个
定时器的超时值,则不用调整
*/
if (!tmp || (timer->expire) < tmp->expire)
{
return;
}
//如果timer是链表的头节点,则将该定时器从链表中取出并重新插入链表
if (timer == head)
{
head = head -> next;
head -> prev = NULL;
timer->next = NULL;
addTimer(timer, head);
}
/*如果timer不是链表的头节点,则该定时器从链表中取出,
然后插入其原来所在位置之后的部分链表*/
else
{
timer->prev->next = timer->next;
timer->next->prev = timer->prev;
addTimer(timer, timer->next);
}
}
//将目标定时器timer从链表中删除
void delTimer(UtilTimer* timer)
{
if (!timer) return;
//下面这个条件成立表示链表中只有一个定时器,即timer
if((timer == head) && (timer == tail))
{
delete timer;
head = tail = NULL;
return;
}
/*
如果链表中至少有两个定时器,且timer是链表的头节点,
则将链表的头节点重置为原头节点的下一个节点,然后删除timer
*/
if (timer == head)
{
head = head -> next;
head -> prev = NULL;
delete timer;
return;
}
/*链表中至少有两个定时器,且timer是链表的尾节点,
则将链表的尾节点重置为原尾节点的前一个节点,然后删除timer
*/
if (timer == tail)
{
tail = head -> prev;
tail -> next = NULL;
delete timer;
return;
}
//如果timer位于链表中间,则把它前后的定时器串联起来,然后删除timer
timer->prev->next = timer->next;
timer->next->prev = timer->prev;
delete timer;
}
/*
SIGALRM信号每次被触发就在其信号处理函数中执行1次tick函数,
以处理链表上到期的任务
*/
void tick()
{
if (!head) return;
printf("timer tick\n");
time_t cur = time(NULL); //获得系统当前时间
UtilTimer* tmp = head;
//从头开始依次处理每个定时器,直到遇到一个尚未到期的定时器,这就是定时器的核心逻辑
while(tmp)
{
/*因为每个定时器都使用绝对时间作为超时值,所以我们可以
把定时器的超时值和系统当前时间,比较以判断定时器是否到期
*/
if (cur < tmp->expire) break;
//调用定时器的回调函数,以执行定时器任务
tmp->cbFunc(tmp->userData);
//执行完定时器中的定时任务后,就将它从链表中删除,并重置链表头节点
head = tmp -> next;
if (head) head->prev = NULL;
delete tmp;
tmp = head;
}
}
private:
/*一个重载的辅助函数,被公有的addTimer函数和adjustTimer函数调用。
该函数表示将timer添加到节点lstHead之后的部分链表中
*/
void addTimer(UtilTimer* timer, UtilTimer* lstHead)
{
UtilTimer* prev = lstHead;
UtilTimer* tmp = prev->next;
/*遍历lstHead节点之后的部分链表,直到找到一个超时时间大于timer的
超时时间的节点,并将timer插入该节点之前
*/
while(tmp)
{
if (timer->expire < tmp->expire)
{
prev->next = timer;
timer->next = tmp;
tmp->prev = timer;
timer->prev = prev;
break;
}
prev = tmp;
tmp = tmp->next;
}
/*如果遍历完lstHead节点之后的部分链表,仍未找到超时时间大于timer的
超时时间的节点,就将timer插入链表的尾部,并把timer设为尾节点
*/
if (!tmp)
{
prev -> next = timer;
timer->prev = prev;
timer->next = NULL;
tail = timer;
}
}
private:
UtilTimer* head;
UtilTimer* tail;
};
#endif
11.2.2 处理非活动连接
服务器程序通常要定期处理非活动连接:给客户端发一个重连请求,或者关闭连接,或者其他。Linux内核提供了对连接是否处于活动状态的定期检查机制,可以通过socket选项KEEPALIVE来激活它。但这种方式不利于管理连接。我们可以考虑在应用层实现类似于KEEPALIVE的机制,以管理所有长时间处于非活动状态的连接。
下面代码利用alarm函数周期性的触发SIGALRM信号,该信号的信号处理函数利用管道通知主循环执行定时器链表上的定时任务--关闭非活动的连接。
闹钟函数:经过seconds秒之后,它会向调用它的进程发送SIGALRM信号,
若该信号没有对应的处理器,则终止进程。
#include <uinstd.h>
unsigned int alarm(unsigned int seconds);
返回值:返回0或以秒为单位的距SIGALRM信号发生所剩的时间
LST_TIMER.cpp
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <libgen.h>
#include <signal.h>
#include <string.h>
#include <assert.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include "timelst.h"
#define FD_LIMIT 65535
#define MAX_EVENT_NUMBER 1024
#define TIMESLOT 5
static int pipefd[2];
static SortTimerList timerList;
static int epollfd = 0;
bool stopServer = false;
bool timeout = false;
int setnonblocking(int fd)
{
int flag = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flag|O_NONBLOCK);
}
//将描述符加入到epollfd中
void addfd(int epfd, int fd)
{
struct epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET; //使用边缘触发模式
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event) == -1)
{
perror("epoll_ctl error");
exit(EXIT_FAILURE);
}
//加入的描述符设为非阻塞
setnonblocking(fd);
}
//简单的信号处理函数
void sigHandler(int sig)
{
printf("capture signal,signal is %d\n",sig);
//保留原来的errno,在函数最后恢复,保证函数的可重入性
int saveErrno = errno;
int msg = sig;
//将信号值发送到管道,通知主循环
if (send(pipefd[1], (char*)&msg, 1, 0) <= 0)
{
printf("The signal sent to the server failed\n");
return;
}
printf("signal is send to server\n");
errno = saveErrno;
}
//为信号设置信号处理函数
void addSig(int sig)
{
struct sigaction act;
bzero(&act, sizeof(act));
act.sa_handler = sigHandler; //设置信号处理函数
sigfillset(&act.sa_mask); //初始化信号屏蔽集
act.sa_flags |= SA_RESTART; //被此信号中断的系统调用自动重启动
//注册信号处理函数
if (sigaction(sig, &act, NULL) == -1)
{
printf("capture signal,but to deal with failure\n");
return;
}
}
//真正的信号处理函数
void handle_sig(int sig)
{
switch(sig)
{
case SIGALRM:
/*用timeout变量标记需要处理,但不立即处理定时任务。
因为定时任务的优先级不是很高,我们有限处理其他更重要的任务
*/
{
timeout = true;
break;
}
case SIGTERM:
stopServer = true;
default:
break;
}
}
void timerHandler()
{
//定时处理任务,实际上就是调用tick函数
timerList.tick();
//一次alarm调用只会发送一次SIGALRM信号,当重新定时后,可以不断的触发SIGALRM信号
alarm(TIMESLOT);
}
//定时器回调函数,它删除非活动连接socket上的注册事件,并关闭之
void cbFunc(ClientData* userData)
{
//将sockfd重监听集合中删除
epoll_ctl(epollfd, EPOLL_CTL_DEL, userData->sockfd, 0);
assert(userData);
close(userData->sockfd);
printf("close fd: %d\n", userData->sockfd);
}
int main()
{
struct sockaddr_in serv_addr;
socklen_t addr_sz;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(12345);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
assert(serv_sock >= 0);
int ret = bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
assert(ret != -1);
ret = listen(serv_sock, 5);
assert(ret != -1);
int epollfd = epoll_create(5);
assert(epollfd != -1);
//将监听文件描述符加入监听集合
addfd(epollfd, serv_sock);
//创建管道
/*sockpair函数创建的管道是全双工的,不区分读写端
此处我们假设pipefd[1]为写端,非阻塞,pipefd[0]为读端
*/
ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd);
assert(ret != -1);
setnonblocking(pipefd[1]);
addfd(epollfd, pipefd[0]); //注册pipefd[0]上的可读事件
//设置信号处理函数
addSig(SIGALRM);
addSig(SIGTERM);
ClientData* users = new ClientData[FD_LIMIT];
alarm(TIMESLOT); //定时
//创建epoll监听集合
struct epoll_event events[MAX_EVENT_NUMBER];
while(!stopServer)
{
bzero(events,sizeof(events));
int event_cnt = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if ((event_cnt < 0) && (errno != EINTR))
{
puts("epoll_wait error");
break;
}
//遍历就绪事件
for(int i=0; i<event_cnt; i++)
{
int sockfd = events[i].data.fd; //获取文件描述符
//如果是服务端套接字,接收客户端的连接
if (sockfd == serv_sock)
{
struct sockaddr_in clnt_addr;
socklen_t clnt_addrlen = sizeof(clnt_addr);
int connfd = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addrlen);
addfd(epollfd, connfd);
users[connfd].address = clnt_addr;
users[connfd].sockfd = connfd;
/*为客户端连接创建定时器,
设置其回调函数与超时时间,然后绑定定时器与用户数据,
最后将定时器添加到链表timerList中
*/
UtilTimer* timer = new UtilTimer;
timer->userData = &users[connfd];
timer->cbFunc = cbFunc;
time_t cur = time(NULL);
timerList.addTimer(timer);
}
//如果是管道的一端有数据可读,那么处理信号
else if ( (sockfd == pipefd[0])&&(events[i].events&EPOLLIN) )
{
char signals[1024] = {0};
int recvRet = recv(pipefd[0], signals, sizeof(signals), 0);
if (recvRet <= 0)
continue;
else
{
//每个信号值占1字节,所以按字节来逐个接收信号
for(int j=0; j<recvRet; j++)
{
printf("server:I caugh the signal %d\n", signals[j]);
handle_sig(signals[j]);
}
}
}
//处理客户端连接上收到的数据
else if (events[i].events & EPOLLIN)
{
memset(users[sockfd].buf, '\0', BUFFER_SIZE);
ret = recv(sockfd, users[sockfd].buf, BUFFER_SIZE-1, 0);
printf("get %d bytes of client data %s from %d\n",
ret, users[sockfd].buf, sockfd);
UtilTimer* timer = users[sockfd].timer;
if (ret < 0)
{
//如果发生读错误,则关闭连接,并移除对应的定时器
if (errno != EAGAIN)
{
cbFunc(&users[sockfd]);
if (timer)
{
timerList.delTimer(timer);
}
}
}
else if (ret == 0)
{
//如果对方关闭连接,则我们也关闭连接,移除对应的定时器
cbFunc(&users[sockfd]);
if (timer)
{
timerList.delTimer(timer);
}
}
else
{
/*如果某个客户连接上有数据可读,要调整该连接
对应的定时器,以延迟该连接被关闭的时间
*/
if (timer)
{
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
printf("adjust timer once\n");
timerList.addjustTimer(timer);
}
}
}
else
{
//其他
}
}
/*最后处理定时事件,因为I/O事件有更高的优先级。
这样可能会导致定时任务不能按照预期时间处理
*/
if (timeout)
{
printf("timeout\n");
timerHandler();
timeout = false;
}
}
close(serv_sock);
close(pipefd[1]);
close(pipefd[0]);
delete []users;
return 0;
}
11.3 I/O复用系统调用的超时参数
由于I/O复用系统调用可能在超时时间到期之前就返回(有I/O事件发生),所以如果我们要利用它们来定时,就需要不断更新定时参数以反映剩余的时间。
#define TIMEOUT 5000
#define MAX_EVENT_NUMBER 1024
int timeout = TIMEOUT;
time_t start = time(NULL);
time_t end = time(NULL);
int epollfd = epoll_create(5);
struct epoll_event events[MAX_EVENT_NUMBER];
while(1)
{
printf("the timeout now is %d mil-seconds\n");
start = time(NULL);
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, timeout);
if ((number < 0) && (errno != EINTR))
{
printf("epoll failture\n");
break;
}
/*超时时间到,此时可处理定时任务,并重置定时时间
*/
if (number == 0)
{
timeout = TIMEOUT;
continue;
}
end = time(NULL);
/*如果epoll_wait返回值大于0,则本次epoll_wait调用持续时间是
(end - start)*1000ms,要将定时时间timeout减去这段时间,以
获得下次epoll_wait调用的超时参数
*/
timeout -= (end - start) * 1000;
/*重新计算之后的timeout值有可能等于0,说明本次epoll_wait调用
返回时,不仅有文件描述符就绪,而且超时时间也刚好到达,此时也要
处理定时任务,并重置定时时间
*/
if (timeout <= 0)
{
timeout = TIMEOUT;
}
//处理其他I/O事件
...
}
11.4 高性能定时器
11.4.1 时间轮
前面介绍的基于升序链表实现的定时器存在一个问题:随着定时器对象的逐渐增加,插入的性能呈线性下降。而时间轮可以有效的解决这个问题。一个简单的时间轮如图所示:
指针指向轮子上的一个槽(slot),它以恒定的速度顺时针转动,每转动一步就指向下一个槽,每次转动称为一个滴答(tick)。每转动一次的时间间隔称为时间轮的槽间隔si(slot interval),总共有N个槽,因此转动一周的时间是N*si。所有的定时器被散列到不同的链表上,每个槽指向一条定时器链表。
假设当前指针指向的槽是cur,待添加的定时器的定时时间为m,则该定时器被插入的链表所对应的槽s为:
为了提高效率,N可以替换为恰好大于N的。这样一来,
可以替换为
&
,即:
=(cur + m) &
假设需要N大于11,那么N应该取值为16()。这样一来,s=(cur+m)&(16-1).
时间轮可以看作是一个哈希表,假设N=16,指针当前所指的槽为13,需要将一个定时时间为10的定时器放入到槽号为6的链表中:
由此可见,使用时间轮,定时器添加的时间复杂度为O(1),执行定时器的时间复杂度为O(N),删除的时间复杂度为O(1)。如果使用多层时间轮,执行的时间复杂度可降为O(1)。
下面是单层时间轮的代码示例。使用数组存放每个槽,使用双向链表将定时器连接起来。当程序检查定时器时,根据时间判断定时器是否到期。如果已到期,则执行相应的操作并删除定时器,否则不做任何处理。
#ifndef TIME_WHEEL_TIMER
#define TIME_WHEEL_TIMER
#include <time.h>
#include <netinet/in.h>
#include <stdio.h>
#define BUFFER_SIZE 64
class TwTimer;
//用户数据结构:客户端socket地址、socket描述符、读缓存和定时器
struct ClientData
{
struct sockaddr_in address;
int sockfd;
char buf[BUFFER_SIZE];
TwTimer* timer;
};
//定时器
class TwTimer
{
public:
TwTimer(int rot, int ts):next(NULL), prev(NULL), rotation(rot), timeSlot(ts) {};
public:
int rotation; //记录定时器在时间轮转多少圈后生效
int timeSlot; //记录定时器属于时间轮上哪个槽
void (*cbFunc)(ClientData*); //定时器回调函数
ClientData* userData; //客户端数据
TwTimer* next; //指向下一个定时器
TwTimer* prev; //指向前一个定时器
};
//时间轮
class TimeWheel
{
public:
TimeWheel():curSlot(0)
{
for (int i = 0; i<N; i++)
{
slots[i] = NULL; //初始化每个槽的头节点
}
}
~TimeWheel()
{
//遍历每个槽,并销毁该槽对应的定时器链表
for(int i = 0; i < N; i++)
{
TwTimer* tmp = slots[i];
while(tmp)
{
slots[i] = tmp->next;
delete tmp;
tmp = slots[i];
}
}
}
//根据定时值timeout创建一个定时器,并把它插入合适的槽中
TimeWheel* addTimer(int timeout)
{
if (timeout < 0) return NULL;
int ticks = 0;
/*下面根据待插入定时器的超时值计算它将在时间轮转动多少个滴答后被触发,并将该滴答数存储与变量ticks中。
如果待插入定时器的超时值小于时间轮的槽间隔SI,则将ticks向上折合为1,否则将ticks向下折合为timeout/SI
*/
if (timeout < SI) ticks = 1;
else ticks = timeout / SI;
//计算定时器在时间轮转动多少圈后被触发
int rotation = ticks / N;
//计算定时器应该被插入哪个槽中
int ts = (curSlot + (ticks % N)) % N;
//创建新的定时器,它在时间轮转动rotation圈后被触发,且位于第ts个槽上
TwTimer* timer = new TwTimer(rotation, ts);
//如果第ts个槽中尚无任何定时器,则把新建的定时器插入其中,并将该定时器设置为该槽的头节点
if (!slots[ts])
{
printf("add timer, rotation is %d, ts is %d, curSlot is %d",
rotation, ts, curSlot);
slots[ts] = timer;
}
//否则,将定时器插入第ts个槽中
else
{
timer->next = slots[ts];
slots[ts]->prev = timer;
slots[ts] = timer;
}
return timer;
}
//删除定时器timer
void delTimer(TwTimer* timer)
{
if (!timer) return;
int ts = timer->timeSlot;
//slots[ts]是timer所在槽的头节点。如果tiemr就是头节点,需要重置第ts个槽的头节点
if (timer == slots[ts])
{
slots[ts] = slots[ts]->next;
if (slots[ts])
{
slots[ts]->prev = NULL;
}
delete timer;
}
else
{
timer->prev->next = timer->next;
if (timer->next)
{
timer->next->prev = timer->prev;
}
delete timer;
}
}
//SI时间到后,调用该函数,时间轮向前转动一个槽的间隔
void tick()
{
TwTimer* tmp = slots[curSlot]; //获取当前槽的头节点
printf("current slot is %d\n", curSlot);
while(tmp)
{
printf("tick ther timer once\n");
//如果定时器的rotation值大于0,说明它还未超时,它的剩余轮次要减1
if (tmp->rotation > 0)
{
tmp->rotation--;
tmp = tmp -> next;
}
else //否则,说明定时器到期,于是执行定时任务,然后删除该定时器
{
tmp->cbFunc(tmp->userData);
//删除定时器,考虑到循环删除,故不用delTimer函数
if (tmp == slots[curSlot])
{
if (tmp == slots[curSlot]) //删除头节点
{
printf("delete header in curSlot\n");
slots[curSlot] = tmp->next;
delete tmp;
if (slots[curSlot])
{
slots[curSlot]->prev = NULL;
}
tmp = slots[curSlot];
}
else //删除非头节点
{
tmp->prev->next = tmp->next;
if (tmp->next)
{
tmp->next->prev = tmp->prev;
}
TwTimer* tmp2 = tmp->next;
delete tmp;
tmp = tmp2;
}
}
}
}
//当前槽处理完后,转向下一个槽,以反映时间轮的转动
curSlot = ++curSlot % N;
}
private:
//时间轮上槽的数目
static const int N = 6;
//每1s时间轮就转动一次,即:槽间隔为1s
static const int SI = 1;
//时间轮的槽,其中每个元素指向一个定时器链表头,链表无序
TwTimer* slots[N];
//时间轮指向的当前槽
int curSlot;
}
#endif