timer的三种实现思路

目录(CSDN目录看看就好了,别点)

1、需求

2、解决思路一

2、解决思路二

4、解决思路三


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对象,待需要执行重传任务,则初始化执行;执行完毕后关闭即可。同时我们还可以通过传参,自定义定时任务的执行函数,比较方便。举例看下面链接中的代码。

wireguard-go/timers.go at master · WireGuard/wireguard-go (github.com)https://github.com/WireGuard/wireguard-go/blob/master/device/timers.go      代码摘选

// 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         这个博主提供了时间轮盘的思路,和思路一差不多,但他是把一个时间范围内的定时任务放在一个时间格子里,粗粒度的。一个时间格子里的定时任务通过链表进行保存。

taskList

         例如,如上图,一个格子是1s,轮盘指针 current 指向当前时间 0s,对于 2s 后发生的任务都放在 2s 的格子里,如果存在多个任务,则使用链表存放。随着时间的流逝,待轮盘指针current指向2s时,取出链表中保存的所有定时任务,按顺序执行。轮盘可以是指针数组实现,支持随机访问,时间复杂度是 O(1)。

        进一步的,作者还提出了二级轮盘。这个分内外盘,内盘时间走一周,外盘走一格(内盘一个1s,外盘一个10s)。二级轮盘相比一级轮盘,在存在大量定时任务而且时间跨度很大的情况下,可以节省大量的存储空间。比如下图中,时间跨度为100s,只需要两个长度为10的数组即可 ,但如果使用一级轮盘则需要长度为100的指针数组。

timewheellevel2

         作者还提供了代码实现,可以去看看,学习学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我要出家当道士

打赏是不可能,这辈子都不可能

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值