目录(CSDN目录看看就好了,别点)
1、需求
在网络开发过程中经常需要创建定时任务,例如特定包的重传,会话的定时删除或者创建定时任务等。而且由于整个项目中可能出现多种类型的定时器任务,所以我们需要设计一种通用的定时器,可以根据定时器任务类别执行相应的操作。
2、解决思路一
在解决这个问题的时候,我并没有参考别人的思路,只是觉得自己的想法很不错,实现起来也比较容易,所以如果你在看了我的文章之后有更好的想法,热烈欢迎评论区参与讨论。下面开始讲解我的思路。
我的想法是,设计一个时间线线程,这个线程按照时间线来执行定时任务(定时任务不存在耗时、阻塞型任务)。这个可以通过链表来实现,链表中每个节点都是一个定时任务,链表的节点都是按照时间进行排序(从小到大),线程死循环读取链表中最近时间节点的任务(即头节点后的第一个节点),读取完任务后如果时间还没到,则阻塞到该时间点(不会影响其他时间节点,因为链表按时间排序)。如果在阻塞等待时间点的过程中,添加了新的节点,则打断线程的阻塞状态,插入节点后重新执行。
由于我的程序有个流量上报和心跳的功能,在程序启动后就会创建这些任务,这些都是由定时器线程来完成的,在时间节点执行完后重置时间点,重新插入链表中。
如果定时器任务完成后,无须再次执行,则可以直接删除。删除也分为两种情况:
1、删除刚执行完的任务,这种情况可以直接header->back = node->back完成删除操作(记得free空间)。
2、删除链表中正在排队的任务,这种情况需要先查找到该节点,再进行删除操作。这样每个任务节点最好有个id,我是去创建节点时的纳秒时间来作为id。
下面我们需要考虑一种极端情况:在将任务节点插入到链表的过程中,其实算一个耗时操作(耗时非常少,但毕竟进行了一些操作,肯定会耽误一定时间),在插入链表的如果我们只进行简单的筛选,如过时的定时任务不执行,并确定时间点(纳秒级),一些及时的定时任务可能无法执行(定时非常短的任务),所以一些情况下,我们对于时间的精度可以要求不那么严格,可以设置一个阈值,设置的时间点前后一定范围内都可以执行(立即执行)。
在程序运行的几个月里,timer表现还算可以,偶尔会出现些问题,但配合定时任务执行逻辑的设计都可以避免。
示例代码(go语言编写,仅供参考)
type Task struct {
Type uint8
Id int64
SockFd int /*发送使用的socket*/
DstIp [4]byte
DstPort int
Data []byte
Count int /*任务的重传次数, 初始为0*/
Time time.Time /*记录该任务的执行时间点(eg. 数据包发送时间点 + 重传周期)*/
}
type Node struct {
TaskInfo Task
Pre *Node
Back *Node
}
//任务执行精度阈值(单位:纳秒)
var THRESHOLD = 1000
//超时时间(单位:毫秒)
var timePeriod = 1000
//重发次数限制
var retransLimit = 3
var ch = make(chan int)
//定时器
var tick = time.NewTicker(1)
var tickSign = time.NewTicker(time.Hour * 24 * 365)
func Execution(header *Node) {
for{
nodeCur, ok := Next(header)
//链表中只剩头节点时为了避免死循环浪费资源,需阻塞
if !ok{
logger.Info("[TimerThread][Execution] : execution is blocking")
<-ch
continue
}
//当前时间点
tNow := time.Now()
//距离任务执行时间还有duration(纳秒)
duration := time.Duration(nodeCur.TaskInfo.Time.UnixNano() - tNow.UnixNano())
if duration >= time.Duration(THRESHOLD){
//阻塞等待执行时间的到来
tick.Reset(duration)
select {
case <- tick.C:
//执行任务
err := taskUnit(header, nodeCur)
if err != nil {
logger.Error("[TimerThread][Execution] : when executing timed task, there is an error happening. Detail info : ", err, " timed Node is : ", nodeCur)
}
//打断阻塞
case <- tickSign.C:
//复原阻塞定时器
tickSign.Reset(time.Hour * 24 * 365)
continue
}
}else if -time.Duration(THRESHOLD) < duration && duration < time.Duration(THRESHOLD){
//立即执行任务
err := taskUnit(header, nodeCur)
if err != nil {
logger.Error("[TimerThread][Execution] : when executing timed task, there is an error happening. Detail info : ", err, " timed Node is : ", nodeCur)
}
}else{
err := taskForNext(header, nodeCur)
if err != nil {
logger.Error("[TimerThread][Execution] : retransmission got some error. Detail info is : ", err)
}
}
}
}
/*
任务的执行单元
*/
func taskUnit(header *Node, node *Node) error {
//重传次数限制
if node.TaskInfo.Count >= retransLimit {
//重传失败操作
return errors.New("the number of retransmissions reaches the upper limit")
}
node.TaskInfo.Count++
if node.TaskInfo.Count == 1{
return nil
}
logger.Warn("[TimerThread][Execution] : Retransmission is in progress! Timed node is : ", node.TaskInfo)
//执行发送任务
err := net.SendTo(node.TaskInfo.SockFd, node.TaskInfo.DstIp, node.TaskInfo.DstPort, node.TaskInfo.Data)
if err != nil{
return err
}
err = taskForNext(header, node)
return err
}
/*
**********************************************
链表操作
**********************************************
*/
/*
新建链表
*/
func New() *Node {
header := Node{
Pre: nil,
Back: nil,
}
return &header
}
/*
向链表中增加节点,按时间排序(从小到大)
*/
func Add(head *Node, node *Node) bool{
//log.Print("add new node: ", node)
//任务时间点检查,过时的任务不予添加
timeNow := time.Now()
if node.TaskInfo.Time.Before(timeNow){
return false
}
//打断正在执行的阻塞
tickSign.Reset(time.Nanosecond * 1)
//查找任务插入位置,pre记录最后一个时间点小于node的节点,cur记录第一个时间点大于node的节点
Mutex.Lock()
tmpPre := head
tmpCur := tmpPre.Back
for tmpCur != nil{
if node.TaskInfo.Time.Before(tmpCur.TaskInfo.Time){
break
}
tmpPre = tmpCur
tmpCur = tmpPre.Back
}
tmpPre.Back = node
node.Back = tmpCur
node.Pre = tmpPre
if tmpCur != nil{
tmpCur.Pre = node
}
Mutex.Unlock()
//链表为空时,存放新的节点后需要激活执行线程
if tmpPre == head && node.TaskInfo.Count == 0{
logger.Debug("[TimerThread][Add] : The timer list is empty, and the execution thread will be waked up. timed node is : ", node)
ch <- 1
}
return true
}
/*
从链表中查找任务执行时间点最近的任务
*/
func Next(header *Node) (*Node, bool) {
Mutex.Lock()
res := header.Back
Mutex.Unlock()
if res == nil{
return nil, false
}
return res, true
}
/*
从链表中删除节点(主动)
*/
func Remove(header *Node,node *Node) (bool, error){
Mutex.Lock()
tmpPre := header
tmpCur := tmpPre.Back
for tmpCur != nil{
if node.TaskInfo.Id == tmpCur.TaskInfo.Id{
break
}
tmpPre = tmpCur
tmpCur = tmpPre.Back
}
if tmpCur == nil{
Mutex.Unlock()
return false, errors.New("[timerThread] : Intent to delete timed node, but not found by id")
}
//如果需要删除正在执行阻塞的节点,需要先打断其阻塞状态
if tmpCur.Pre == header{
tickSign.Reset(time.Nanosecond * 1)
}
if tmpCur.Back != nil{
tmpCur.Back.Pre = tmpPre
}
tmpPre.Back = tmpCur.Back
Mutex.Unlock()
return true, nil
}
/*
从链表中删除节点
*/
func taskForNext(header *Node, node *Node) error{
Mutex.Lock()
if node.Back != nil{
node.Back.Pre = node.Pre
}
node.Pre.Back = node.Back
Mutex.Unlock()
node.TaskInfo.Time = time.Now().Add(time.Millisecond * time.Duration(timePeriod))
node.Pre = nil
node.Back = nil
Add(header, node)
return nil
}
2、解决思路二
第二个思路是wireguard里面提供的,每个定时任务都由一个timer线程来维护。这样做的好处时,timer之间相互隔离,互不影响。可以在需要执行重传任务的对象里定义一个timer对象,待需要执行重传任务,则初始化执行;执行完毕后关闭即可。同时我们还可以通过传参,自定义定时任务的执行函数,比较方便。举例看下面链接中的代码。
// A Timer manages time-based aspects of the WireGuard protocol.
// Timer roughly copies the interface of the Linux kernel's struct timer_list.
type Timer struct {
*time.Timer
modifyingLock sync.RWMutex
runningLock sync.Mutex
isPending bool
}
func (peer *Peer) NewTimer(expirationFunction func(*Peer)) *Timer {
timer := &Timer{}
timer.Timer = time.AfterFunc(time.Hour, func() {
timer.runningLock.Lock()
defer timer.runningLock.Unlock()
timer.modifyingLock.Lock()
if !timer.isPending {
timer.modifyingLock.Unlock()
return
}
timer.isPending = false
timer.modifyingLock.Unlock()
expirationFunction(peer)
})
timer.Stop()
return timer
}
4、解决思路三
Go语言中时间轮的实现 - luozhiyun`s Bloghttps://www.luozhiyun.com/archives/444 这个博主提供了时间轮盘的思路,和思路一差不多,但他是把一个时间范围内的定时任务放在一个时间格子里,粗粒度的。一个时间格子里的定时任务通过链表进行保存。
例如,如上图,一个格子是1s,轮盘指针 current 指向当前时间 0s,对于 2s 后发生的任务都放在 2s 的格子里,如果存在多个任务,则使用链表存放。随着时间的流逝,待轮盘指针current指向2s时,取出链表中保存的所有定时任务,按顺序执行。轮盘可以是指针数组实现,支持随机访问,时间复杂度是 O(1)。
进一步的,作者还提出了二级轮盘。这个分内外盘,内盘时间走一周,外盘走一格(内盘一个1s,外盘一个10s)。二级轮盘相比一级轮盘,在存在大量定时任务而且时间跨度很大的情况下,可以节省大量的存储空间。比如下图中,时间跨度为100s,只需要两个长度为10的数组即可 ,但如果使用一级轮盘则需要长度为100的指针数组。
作者还提供了代码实现,可以去看看,学习学习。