一文搞定Linux的定时器(19)

定时器是什么

网络程序需要处理的第三类事件是定时事件,比如定期检测一个客户连接的活动状态。服务器程序通常管理着众多定时事件,因此有效地组织这些定时事件,使之能在预期的时间点被触发且不影响服务器的主要逻辑,对于服务器的性能有着至关重要的影响。为此,我们要将每个定时事件分别封装成定时器,并使用某种容器类数据结构,比如链表、排序链表和时间轮,将所有定时器串联起来,以实现对定时事件的统一管理。本博客主要讨论的就是两种高效的管理定时器的容器:
时间轮和时间堆。

首先什么是定时

不过,在讨论如何组织定时器之前,我们先要介绍定时的方法。定时是指在一段时间之后触发某段代码的机制,我们可以在这段代码中依次处理所有到期的定时器。换言之,定时机制是定时器得以被处理的原动力。Linux提供了三种定时方法,它们是:
socket选项SO_RCVTIMEO 和so_SNDTIMEO。
SIGALRM信号。
IO复用系统调用的超时参数

SO_RCVTIMEO 和so_SNDTIMEO

这个博客中我们介绍过socket选项So_RCVTIMEO和SO_SNDTIMEO,它们分别用来设置socket接收数据超时时间和发送数据超时时间。因此,这两个选项仅对与数据接收和发送相关的socket专用系统调用有效,这些系统调用包括send、sendmsg、recv、recvmsg、accept和 connect。我们将选项SO_RCVTIMEO和SO_SNDTIMEO对这些系统调用的影响总结于下表中

系统调用有效选项系统调用超时后的行为
sendSO_SNDTIMEO返回-1,设置errno为EAGAIN或EWOULDBLOCK
sendmsgSO_SNDTIMEO返回-1,设置errno为EAGAIN或EWOULDBLOCK
recvSO_RCVTIMEO返回-1,设置errno为EAGAIN或EWOULDBLOCK
recvmsgSO_RCVTIMEO返回-1,设置errno为EAGAIN或EWOULDBLOCK
accpteSO_RCVTIMEO返回-1,设置errno为EAGAIN或EWOULDBLOCK
connectSO_SNDTIMEO返回-1,设置errno为EINPROGRRRESS

SIGALRM

由alarm和 setitimer函数设置的实时闹钟一旦超时,将触发SIGALRM信号。因此,我们可以利用该信号的信号处理函数来处理定时任务。但是,如果要处理多个定时任务,我们就需要不断地触发SIGALRM信号,并在其信号处理函数中执行到期的任务。一般而言,SIGALRM信号按照固定的频率生成,即由alarm或setitimer函数设置的定时周期T保持不变。如果某个定时任务的超时时间不是T的整数倍,那么它实际被执行的时间和预期的时间将略有偏差。因此定时周期T反映了定时的精度。

高性能定时器

时间轮(基于hash表实现)

时间轮如图:

在这里插入图片描述
上图图所示的时间轮内,(实线)指针指向轮子上的一个槽(slot)。它以恒定的速度顺时针转动,每转动一步就指向下一个槽(虚线指针指向的槽),每次转动称为一个滴答( tick)。一个滴答的时间称为时间轮的槽间隔si (slot interval),它实际上就是心搏时间。该时间轮共有N个槽,因此它运转一周的时间是Nsi。每个槽指向一条定时器链表,每条链表上的定时器具有相同的特征:**它们的定时时间相差Nsi 的整数倍**。时间轮正是利用这个关系将定时器散列到不同的链表中。
假如现在指针指向槽cs,我们要添加一个定时时间为ti的定时器,则该定时器将被插入槽ts ( timer slot)对应的链表中:
ts=(cs+(ti/si))%N
基于排序链表的定时器使用唯一的一条链表来管理所有定时器**,所以插入操作的效率随着定时器数目的增多而降低**。而时间轮使用哈希表的思想,将定时器散列到不同的链表上。这样每条链表上的定时器数目都将明显少于原来的排序链表上的定时器数目,插人操作的效率基本不受定时器数目的影响。

笔者将实现时间轮代码封装一下 (未完成)

时间堆(最小堆思路)

设计定时器的另外一种思路是﹔将所有定时器中超时时间最小的一个定时器的超时值作为心搏间隔。这样,一旦心搏函数tick被调用,超时时间最小的定时器必然到期,我们就可以在tick 函数中处理该定时器。然后,再次从剩余的定时器中找出超时时间最小的一个,并将这段最小时间设置为下一次心搏间隔。如此反复,就实现了较为精确的定时。
在这里插入图片描述
树的基本操作是插人节点和删除节点。对最小堆而言,它们都很简单。为了将一个元素X插入最小堆,我们可以在树的下一个空闲位置创建一个空穴。如果X可以放在空穴中而不破坏堆序,则插入完成。否则就执行上虑操作,即交换空穴和它的父节点上的元素。不断执行上述过程,直到X可以被放入空穴,则插入操作完成。比如,我们上图要往图所示的最小堆中插入值为14的元素,则可以按照下图所示的步骤来操作
在这里插入图片描述
最小堆的删除操作指的是删除其根节点上的元素,并且不破坏堆序性质。执行删除操作时,我们需要先在根节点处创建一个空穴。由于堆现在少了一个元素,因此我们可以把堆的最后一个元素X移动到该堆的某个地方。如果X可以被放入空穴,则删除操作完成。否则就执行下虑操作,即交换空穴和它的两个儿子节点中的较小者。不断进行上述过程,直到X可以被放入空穴,则删除操作完成。比如,我们
上上图来实现delete
在这里插入图片描述
由于最小堆是一种完全二叉树,所以我们可以用数组来组织其中的元素。比如,所示的最小堆可以用下图所示的数组来表示。对于数组中的任意一个位置i上的元素,其左儿子节点在位置24+1上,其右儿子节点在位置2i+2上,其父节点则在位置〔(i-1)/2 ](i0)上。与用链表来表示堆相比,用数组表示堆不仅节省空间,而且更容易实现堆的插人、删除等操作
在这里插入图片描述
假设我们已经有一个包含N个元素的数组,现在要把它初始化为一个最小堆。那么最简单的方法是﹔
初始化一个空堆,然后将数组中的每个元素插人该堆中。不过这样做的效率偏低。实际上,**我们只需要对数组中的第〔(N-1)/2]0个元素执行下虑操作,即可确保该数组构成一个最小堆**。这是因为对包含N个元素的完全二叉树而言,它具有[(N-1)/2]个非叶子节点,这些非叶子节点正是该完全二叉树的第0[(N-1)/2]个节点。我们只要确保这些非叶子节点构成的子树都具有堆序性质,整个树就具有堆序性质。
笔者实现封装时间堆(未完成)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值