timer
timer copied to clipboard
目标和参考资料
目标
- 用go使用5级时间轮实现定时器
- 如有可能效率追求fast fast fast
参考资料
- https://blog.csdn.net/musicml/article/details/103878367 (good)
- https://www.zhihu.com/question/38427301 (good)
- https://www.cnblogs.com/newbeeyu/p/9022623.html(good)
论文
http://www.cs.columbia.edu/~nahum/w6998/papers/ton97-timing-wheels.pdf
参考项目
- linux kernel https://github.com/torvalds/linux/blob/v4.7/kernel/time/timer.c (linux 2.6内核引入时间轮)
- skynet https://github.com/cloudwu/skynet/blob/master/skynet-src/skynet_timer.c
- timingwheel (借鉴API名称) https://github.com/RussellLuo/timingwheel
参考书本
-
书名 深入linux内核架构(Professional Linux Kernel Architecture)(第15章时间管理) tv1 0-255
-
范围 tv2 2的8次方-2的14次方-1 tv3 2的14次方-2的20次方-1 tv4 2的20次方-2的26次方-1 tv5 2的26次方-2的32次方-1
-
移动 第一组的内容在最多256个时间周期之后就会耗尽,必须将后续各组的定时器依次前推,重新补足第一组。在第一组的索引位置恢复到初始位置0之后,会将第二组中一个数组项的所有定时器补充到第一组。这种做做法,解释了为什么各组选择了不同的时间间隔。因为第一组的各数组项可能有256个不同的到期时间,而第二组中一个数组项的数据就足以填充整个第一组的整个数组。该道理同样适用于后续各组。第三组的一个数组项的数据同样足以填充整修第二个组,第四组的一个数组项也足以填充整修第三组,而第五组的一个数组项也足以填充整个第四组。
后续各组的数组位置并非随机选择的,其中的索引项仍然发挥了作用。但索引项的值不再是每个时钟周期加1,而是第256的i-1次方
- cascade函数用于从指定组取得定时器补充到前一组
感谢。、
@kingname :blush:
我的使用场景是这样,面对很多网络连接,先声明一个NewTimer ,然后用一个协程去Run,而后每一个连接打开的时候,使用TM.ScheduleFunc() 对每个连接进行不同时间的心跳,测试的时候是每个连接到来,每5秒下发一次心跳,一个连接没问题,超过一个连接,其他的连接发送了几次心跳后会莫名停止,就是TM.ScheduleFunc中执行的函数不执行了,众多连接只有一个连接能发送心跳,其他的都不能,请赐教,是不是我用的方式不对。
@jungeshidai go.mod是v0.0.4吧?可否发下调用代码,我测试下。
@jungeshidai go.mod是v0.0.4吧?可否发下调用代码,我测试下。我使用的版本是1.14 ,随便写了个测试代码。调试工具是(以太网调试助手SocketTool_NoAD),很多连接时,每个连接大约都能跑1分钟所有,过了一分钟,只剩下一个连接能发送心跳了。
package main
import (
"encoding/hex"
"flag"
"fmt"
"github.com/antlabs/timer"
"net/http"
_ "net/http/pprof"
"log"
"time"
"github.com/panjf2000/gnet"
)
var (
SetBytes, _ = hex.DecodeString("01030000001445C5")
TM = timer.NewTimer()
)
type iotServer struct {
*gnet.EventServer
}
func (es *iotServer) OnInitComplete(srv gnet.Server) (action gnet.Action) {
log.Printf("Iot server is listening on %s (multi-cores: %t, loops: %d)\n", srv.Addr.String(), srv.Multicore, srv.NumEventLoop)
go ProfStart()
//启动时间轮
go TimeStart()
return
}
/*
消息处理函数
*/
func (es *iotServer) React(data []byte, c gnet.Conn) (out []byte, action gnet.Action) {
return
}
/*
连接打开时
*/
func (es *iotServer) OnOpened(c gnet.Conn) (out []byte, action gnet.Action) {
TM.ScheduleFunc(5*time.Second, func() {
fmt.Printf("开始下发指令:[%s]\n", c.RemoteAddr())
if c == nil {
return
}
err := c.AsyncWrite(SetBytes)
fmt.Printf("时间[%s]:[%s]:下发指令:%x\n", time.Now().Format("2006-01-02 15:04:05"), c.RemoteAddr(), SetBytes)
if err != nil {
c.Close()
}
})
return
}
/*
连接退出时
*/
func (es *iotServer) OnClosed(c gnet.Conn, err error) (action gnet.Action) {
return
}
/*
程序性能监控模块启动
*/
func ProfStart() {
http.ListenAndServe("0.0.0.0:6060", nil)
}
/*
时间轮模块启动
*/
func TimeStart() {
TM.Run()
}
func main() {
var port int
var multicore bool
flag.IntVar(&port, "port", 501, "--port 9000")
flag.BoolVar(&multicore, "multicore", true, "--multicore true")
flag.Parse()
iot := new(iotServer)
log.Fatal(gnet.Serve(iot, fmt.Sprintf("tcp://:%d", port), gnet.WithMulticore(multicore)))
}
ok,我看下
@jungeshidai 下载v0.0.5版本看下呢,刚刚处理了下周期性定时器边界问题。
@guonaihong 目前测试看,问题已经修复,之前的0.4版本我已经再测试环境测试了性能问题,长期运行CPU占用0.7左右的。马上发布0.5的版本到测试服务器上测试,有问题再向您反馈,感谢。
@jungeshidai ok,客气了。 对于上面的代码有个建议,在tcp连接关闭时,可以把这个tcp绑定的time wheel node也关闭。ScheduleFunc会返回time wheel节点对象,调用Stop()就是关闭。
提问题可以建新的issue。一个问题一个issue,这样以后也可以帮助到别人检索问题和帮助。