排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 排序方式 | 稳定性 |
---|---|---|---|---|---|---|
冒泡排序 | 内排序 | 稳定 | ||||
选择排序 | 内排序 | 不稳定 | ||||
插入排序 | 内排序 | 稳定 | ||||
希尔排序 | 内排序 | 不稳定 | ||||
归并排序 | 外排序 | 稳定 | ||||
快速排序 | 内排序 | 不稳定 | ||||
堆排序 | 内排序 | 不稳定 | ||||
计数排序 | 外排序 | 稳定 | ||||
桶排序 | 外排序 | 稳定 | ||||
基数排序 | 外排序 | 稳定 |
信号是一种异步事件,信号处理函数和程序的主循环是两条不同的执行路线。很显然,信号处理函数需要尽可能快地执行完毕,以确保该信号不被屏蔽太久。一种典型地解决方案是:把信号的主要处理逻辑放在程序的主循环中,当信号处理函数被触发时,它只是简单地通知主循环程序接收到信号,并把信号值传递给主循环,主循环再根据接收到的信号值执行目标信号对应的逻辑代码。信号处理函数通常使用管道来将信号“传递”给主循环:信号处理函数往管道写端写入信号值,主循环则从管道的读端读出该信号值。那么主循环如何知道管道上有数据可读?通过使用 I/O复用系统调用来监听管道的读端文件描述符上的可读时间。如此一来,信号事件就能和其他I/O事件一样被处理,即统一事件源
一般而言,SIGALRM 信号按照固定的频率生成,即由 alarm 或 setitimer 函数设置的定时周期 T 保持不变。如果某个定时任务的超时时间不是 T 的整数倍,那么它实际被执行的时间和预期的时间将略有偏差。因此定时周期 T 反映了定时的精度
定时器通常至少要包含两个成员:一个超时时间(相对时间或者绝对时间)和一个任务回调函数。有的时候还可能包含回调函数被执行时需要传入的参数,以及是否重启定时器等信息。如果使用链表作为容器来串联所有的定时器,则每个定时器还要包含指向下一个定时器的指针成员。进一步,如果链表是双向的,则每个定时器还需要包含指向前一个定时器的指针成员。
/*用户数据结构:客户端socket地址、socket文件描述符、读缓存和定时器*/
struct client_data{};
/*定时器类 其public成员需要包括定时器任务的超时时间;
任务回调函数 回调函数处理的是客户的数据,由定时器的执行者传递给回调函数;
用于指向前后定时器的指针
*/
class sort_timer_lst{};
/*
定时器链表。是一个升序、双向链表,且带有头节点和尾结点
构造函数和析构函数的实现,链表被销毁时,删除其中所有的定时器
*/
class sort_timer_lst{
public:
sort_timer_lst():head(NULL), tail(NULL){}
~sort_timer_lst(){...}
/* 将目标定时器timer添加到链表中 */
void add_timer( util_timer* timer ){
...
/*如果目标定时器的超市时间小于当前链表中所有定时器的超时时间,则把该定时器插入链表头部,作为 新链表的头节点,
否则调用重载函数 add_timer(util_timer* timer, util_timer* lst_head), 保证链表的升序特性*/
...
add_timer( timer, head );
}
/*当任务发生变化,调整对应位置*/
void adjust_timer( util_timer* timer ){
...
/*需要注意目标定时器当前位置,如果是链表的头节点,则该定时器从链表中取出并重新插入链表*/
if ( timer == head ){ ... add_timer(timer, head); }
/*如果不是链表头节点,取出并插入其原来所在位置之后的部分链表中*/
else { ... add_timer(timer, timer->next); }
}
/*删除定时器timer*/
void del_timer( util_timer* timer ){
...
}
/*SGIALRM信号每次被触发就在其信号处理函数 (如果使用同一事件源,则是主函数) 中执行一次 tick 函数,以处理链表上到期的任务 */
void tick(){
...
printf("timer tick\n");
time_t cur = time(NULL);
util_timer* tmp = head;
...
/*从头节点开始处理每个定时器,直到遇到一个尚未到期的定时器,就是定时器的核心逻辑*/
while(tmp){
...
}
}
private:
/*一个重载的辅助函数,被公有的add_timer函数和adjust_timer函数调用*/
void add_timer(util_timer* timer, util_timer* lst_head){...}
private:
util_timer* head;
util_timer* tail;
}
基于排序链表的定时器存在一个问题:添加定时器的效率偏低。而解决办法包括时间轮,时间堆。
如图所示,(实线)指针指向轮子上的槽(slot)。以恒定的速度顺时针转动,每转动一步就指向下一个槽(虚线指针指向的槽),每次转动称为一个滴答(tick)。一个滴答的时间称为时间轮的槽间隔 si (slot interval), 它实际上就是心搏时间。该时间轮共有 N 个槽,因此它转动一周的时间是 N*si。每个槽指向一个条定时器链表,每条链表上的定时器具有相同的特征:它们的定时时间相差 N*si 的整数倍。时间轮利用的便是==散列==的思想。所以需要添加一个定时时间为 ti 的定时器,则该定时器将被插入槽 ts(timer slot) 对应的链表中:
基于排序链表的定时器使用唯一的一条链表来管理所有的定时器,所以插入操作的效率随着定时器数目的增多而降低。而时间轮使用哈希表的思想,将定时器散列到不同的链表上。这样每条链表上的定时器数目都将明显少于原来的排序链表上的定时器数目,插入操作的效率基本不受定时器数目的影响。
显然,对于时间轮来说,要提高定时精度,就要使 si 值足够小,要提高执行效率,则要求 N 值足够大
除了以上以固定频率调用心搏函数 tick()
的定时方案,另一种思路是:将所有定时器中超时时间最小的一个定时器的超时值作为心搏间隔。这样,一旦心搏函数 tick()
被调用,超时时间最小的定时器必然到期, 我们就可以在 tick()
函数中处理该定时器。然后,再次从剩余的定时器中找出超时时间最小的一个,并将这段最小时间设置为下一次心搏间隔。如此反复,就实现了较为精确的定时
- Timer_LinkList.h
- Handle_None_live_LinkList.cpp
- Timer_Wheel.h
- Handle_None_live_Wheel.cpp
- Timer_Heap.h
- Handle_None_live_Heap.cpp