定时器模块是服务器中的常用组件,本文带你实现一个具有基本功能的定时器模块要想设计一个定时器模块,一般包含两部分,一个是定时器对象(ServerTimer),另一个管理定时器对象的管理者(TimerManager)也叫定时器容器;定时器使用了C#内System库里面的Timer定时器作为定时器的主驱动
2 定时器对象设计一个Timer对象就是对一个定时器的包装
过期年月日时分秒、执行间隔、定时事件回调是核心的基本成员。
定时器类型是为了区分每个定时器的类型,在更新的时候好执行对应的逻辑,
定时器一共分成了6中类型
Once:在指定的年月日时分秒调起Timer内的回调
FixedInterlval:固定周期调用Timer内的回调
EveryDay/EveryWeek/EveryMonth/EveryYear:日/周/月/年定时器
ServerTimerRunType.cs实现
public enum ServerTimerRunType
{
//一次性 定时器
Once,
//固定时间 循环定时器
FixedInterval,
//天 定时器
EveryDay,
//周 定时器
EveryWeek,
//月 定时器
EveryMonth,
//年 定时器
EveryYear
}
ServerTimer 实现
using System;
using System.Collections.Generic;
using System.Text;
using Servers.Core;
namespace Servers.Common
{
public class ServerTimer
{
//运行类型
public ServerTimerRunType RunType { get; private set; }
//年
public int Year { get; private set; }
//月
public int Month { get; private set; }
//周几
public DayOfWeek WeekDay { get; private set; }
//几号
public int Day { get; private set; }
//时
public int Hour { get; private set; }
//分
public int Minute { get; private set; }
//秒
public int Second { get; private set; }
//间隔(秒)
public int Interval { get; private set; }
//当前间隔
public int CurrIntervalSecond { get; private set; }
//执行回调
public Action OnDoAction { get; private set; }
//构造函数
public ServerTimer(ServerTimerRunType runType, Action ondoaction,
int year = 0, int month = 0, int day = 0, int hour = 0,
int minute = 0, int second = 0, int interval = 0,
DayOfWeek weekday = DayOfWeek.Monday)
{
RunType = runType;
Year = year;
Month = month;
Day = day;
Hour = hour;
Minute = minute;
Second = second;
Interval = interval;
WeekDay = weekday;
OnDoAction = ondoaction;
}
public void DoAction()
{
OnDoAction?.Invoke();
}
public void ServerTimerTick()
{
++CurrIntervalSecond;
if (CurrIntervalSecond >= Interval)
{
CurrIntervalSecond = 0;
DoAction();
}
}
}
}
3 定时器管理者设计
我们现在可以在主程序中创建n个定时器了,但是怎么管理它呢?如何添加一个新的定时器、手动删除一个旧的定时器、检查这些定时器是否已经到期?如果把这部分代码放到主程序中,那么也太繁杂了,所以就对代码分层写了一个TimerMananger类;这个类负责计时器的更新、添加、删除;一个TimeManager对象管理该线程内的所有定时器对象,所以必有一个数据结构来组织这些定时器对象,我们这里使用的链表结构,不过这种结构的缺点是每次都需要重头遍历,如果ServerTimer的实例过多的话对导致很多次不必要的循环。后续会改成小顶堆的计时器。
TimerManager.cs实现
using System;
using System.Collections.Generic;
using System.Text;
using System.Timers;
using Servers.Core;
namespace Servers.Common
{
public class TimerManager
{
//服务器开始运行的tick时间(1 tick = 1/一千万 秒)
private static double m_BeginTickTime;
//服务器运行时间(秒)
public static float RunTime
{
get
{
return (float)(DateTime.UtcNow.Ticks - m_BeginTickTime) / 10000000;
}
}
//秒定时器
private static Timer m_SecondTimer;
//定时器链表
private static LinkedList m_ServerTimers;
//当前年
private static int m_CurrYear;
//当前余额
private static int m_CurrMonth;
//当前日
private static int m_CurrDay;
//当前周几
private static DayOfWeek m_CurrWeekDay;
//当前时
private static int m_CurrHour;
//当前分
private static int m_CurrMinute;
//当前秒
private static int m_CurrSecond;
public static void Init()
{
m_ServerTimers = new LinkedList();
m_SecondTimer = new Timer();
m_SecondTimer.Elapsed += SecondTimerElapsed;
m_SecondTimer.Enabled = true;
m_SecondTimer.Interval = 1000;
m_BeginTickTime = DateTime.UtcNow.Ticks;
Console.WriteLine(" TimerManager.Init Complete");
}
private static void SecondTimerElapsed(object sender, ElapsedEventArgs e)
{
//Console.WriteLine(" One Second timeout");
DateTime currTime = DateTime.Now;
m_CurrYear = currTime.Year;
m_CurrMonth = currTime.Month;
m_CurrDay = currTime.Day;
m_CurrWeekDay = currTime.DayOfWeek;
m_CurrHour = currTime.Hour;
m_CurrMinute = currTime.Minute;
m_CurrSecond = currTime.Second;
//拉到所有的计时器对象
ServerTimer serverTimer;
LinkedListNode iter = m_ServerTimers.First;
while (null != iter)
{
serverTimer = iter.Value;
switch (serverTimer.RunType)
{
//TODO:Once定时器存在的问题,如果链表里Once内容特别多的话,执行了之后仍任不删除,会导致循环次数增加,如果数量巨大会给cpu造成一些压力
case ServerTimerRunType.Once:
{
if (serverTimer.Year == m_CurrYear && serverTimer.Month == m_CurrMonth && serverTimer.Day == m_CurrDay &&
serverTimer.Hour == m_CurrHour && serverTimer.Minute == m_CurrMinute && serverTimer.Second == m_CurrSecond)
{
serverTimer.DoAction();
}
}
break;
case ServerTimerRunType.FixedInterval:
{
serverTimer.ServerTimerTick();
}
break;
case ServerTimerRunType.EveryDay:
{
if (serverTimer.Hour == m_CurrHour && serverTimer.Minute == m_CurrMinute && serverTimer.Second == m_CurrSecond)
{
serverTimer.DoAction();
}
}
break;
case ServerTimerRunType.EveryWeek:
{
if (serverTimer.WeekDay == m_CurrWeekDay &&
serverTimer.Hour == m_CurrHour && serverTimer.Minute == m_CurrMinute && serverTimer.Second == m_CurrSecond)
{
serverTimer.DoAction();
}
}
break;
case ServerTimerRunType.EveryMonth:
{
if (serverTimer.Day == m_CurrDay &&
serverTimer.Hour == m_CurrHour && serverTimer.Minute == m_CurrMinute && serverTimer.Second == m_CurrSecond)
{
serverTimer.DoAction();
}
}
break;
case ServerTimerRunType.EveryYear:
{
if (serverTimer.Month == m_CurrMonth && serverTimer.Day == m_CurrDay &&
serverTimer.Hour == m_CurrHour && serverTimer.Minute == m_CurrMinute && serverTimer.Second == m_CurrSecond)
{
serverTimer.DoAction();
}
}
break;
}
iter = iter.Next;
}
}
public static void RegisterServerTime(ServerTimer timer)
{
m_ServerTimers.AddLast(timer);
}
public static void RemoveServerTimer(ServerTimer timer)
{
m_ServerTimers.Remove(timer);
}
}
}
4 总结
可以看到定时器模块本身逻辑并不复杂,最重要的是要考虑效率的问题,采用何种数据结构,来使得以上三种基本操作的时间复杂度较小;
常用的数据结构:
- 链表、队列
- map
- 时间轮
- 时间堆
我们这里使用的链表结构,不过这种结构的缺点是每次都需要重头遍历,然后一个个的比对,如果数量超过10W级性能会急剧下降。如果ServerTimer的实例过多的话对导致很多次不必要的循环。所以在后面我会把这个计时器优化成小顶堆的计时器,性能会有明显的提升。
这里稍微讲一下小顶堆的计时器。小队顶计时器其实就是使用了堆排序的原理。
定时器是一个树状结构,每次插入或者是超时的时候都会对堆重新进行排序,更新的时候只取对顶上(第0个)的元素,取到的一定是时间最少的Timer,当这个计时器超时的时候,将堆再次排序,排序完成后判断下一个是否超时,如果超时了循环以上步骤,如果没超时,这次检测结束。