前言
最近了解了时间轮定时器原理和linux内核用c语言实现的时间轮定时器源码之后。按照自己的理解,突发奇想 自己用C#来实现一遍。
正题
时间轮定时器的组成就分两个部分。
一是任务部分,WheelTask.cs。
二是时间轮部分,和TimeWheel.cs。
以下代码用的是 .Net6.0 框架的。
首先来看任务部分。
WheelTask类里面存储一些必要的信息:
1.任务id (方便日后移除任务)、
2.延迟时间、
3.时间间隔、
4.循环次数、
5.执行时间、
6.执行函数、
7.已循环次数。
这些没什么好说的,都是字面意思。
然后初始化对象时,执行时间就取当前时间+延迟时间。
dateTime = DateTime.Now;
dateTime = dateTime.AddMilliseconds(this.delay);
执行任务的话有两种方式,看自己需要是想同步还是异步执行。
第一种,时间到了之后执行,等待执行完后再算后面循环的时间戳,也就是阻塞线程的。
public void Run()
{
action();
}
第二种,时间到了之后执行,但是不会等待它执行完就计算后面要循环的时间戳,也就是不阻塞线程的。
public async Task RunAsync()
{
await Task.Run(() =>
{
action();
});
}
检查循环,每次调用了Run()或者RunAsync()之后执行CheckLoop()方法。
如果还有要循环的次数,那么就刷新dateTime,将其改为当前时间+时间间隔,也就是下一次执行的时间。
public bool CheckLoop()
{
m_hadLoopTimes++;
if (loopTimes == -1)
{
dateTime = DateTime.Now.AddMilliseconds(interval);
return true;
}
else if(loopTimes > 0)
{
if (loopTimes > m_hadLoopTimes)
{
dateTime = DateTime.Now.AddMilliseconds(interval);
return true;
}
}
return false;
}
接下来看时间轮部分。
TimeWheel类的代码较多,这里先逐个讲解。
首先,我们知道肯定要有一个while循环来驱动时间轮运转,所以我们定义一个Update方法用来更新时间轮。
public void Update()
{
lock (s_obj)
{
DateTime now = DateTime.Now;
//下面这一块只是为了打印,不用管它
if (now.Ticks - m_temp >= 10000000)
{
Console.WriteLine("当前时间 " + now);
m_temp = now.Ticks;
}
//TODO... 一些时间轮调度的逻辑
}
}
那么这个Update方法在哪里调用呢,在这里就先插一嘴提提主程序入口的样子。
因为while循环会阻塞线程。所以,在主程序入口里面,我们就开启一个新的线程来while循环,保证不卡主线程,while循环里面就疯狂调用Update方法。
using TimeWheelProject;
internal class Program
{
public static TimeWheel timeWheel;
private static Thread thread;
private static void Main(string[] args)
{
timeWheel = new TimeWheel();
thread = new Thread(TimeWheelProgram);
thread.Start();
Console.ReadLine();
}
public static void TimeWheelProgram()
{
while (true)
{
timeWheel.Update();
}
}
}
在Update()方法里面,要处理的最核心逻辑就是关于时间轮的调度,也就是将任务WheelTask不断推到最终的执行轮(毫秒轮)上,再执行任务。
那么,关于轮的定义,如下图所示:
上面举例看到,时、分、秒三个轮分别按照平时的钟表刻度来进行分割。
在代码中,不止时、分、秒这3个轮,还有年、月、日、毫秒轮。
每个轮盘用字典Dictionary存储:
key值是分割数(或者叫槽位数),月、日、时、分、秒轮都是按正常时间刻度分,只有年轮、毫秒轮按自定义分。
value是WheelTask类型的双链表LinkedList
/// <summary>年轮 </summary>
private Dictionary<int, LinkedList<WheelTask>> wheelYear;
/// <summary>月轮 </summary>
private Dictionary<int, LinkedList<WheelTask>> wheelMonth;
/// <summary>日轮 </summary>
private Dictionary<int, LinkedList<WheelTask>> wheelDay;
/// <summary>时轮 </summary>
private Dictionary<int, LinkedList<WheelTask>> wheelHour;
/// <summary>分轮 </summary>
private Dictionary<int, LinkedList<WheelTask>> wheelMinute;
/// <summary>秒轮 </summary>
private Dictionary<int, LinkedList<WheelTask>> wheelSecond;
/// <summary>毫秒轮 </summary>
private Dictionary<int, LinkedList<WheelTask>> wheelMillisecond;
那么他们的分割情况具体是怎样的,请看下面:
定义常量
private const int k_Level6 = 100; //先来个百年奋斗目标,没人会定超过100年的定时器吧?
private const int k_Level5 = 12; //1年即12个月 分割成12份
private const int k_Level4 = 31; //1个月即31天 分割成31份
private const int k_Level3 = 24; //1天即24小时 分割成24份
private const int k_Level2 = 60; //1小时和1分钟 都分割成60份
private const int k_Level1 = 10; //1秒即1000毫秒 分割成10份,每份100毫秒
根据常量,初始化每个轮盘字典的槽位
为什么毫秒轮不按毫秒刻度1000来分?因为我不想让轮盘存的链表数量过多,所以只分了10份。
//轮盘最大刻度,就从2023年开始到2123年吧 嘻嘻
Enumerable.Range(2023, 2023 + k_Level6).ToList().ForEach(x =>
{
wheelYear.Add(x, new LinkedList<WheelTask>());
});
Enumerable.Range(0, k_Level5).ToList().ForEach(x =>
{
wheelMonth.Add(x, new LinkedList<WheelTask>());
});
Enumerable.Range(0, k_Level4).ToList().ForEach(x =>
{
wheelDay.Add(x, new LinkedList<WheelTask>());
});
Enumerable.Range(0, k_Level3).ToList().ForEach(x =>
{
wheelHour.Add(x, new LinkedList<WheelTask>());
});
Enumerable.Range(0, k_Level2).ToList().ForEach(x =>
{
wheelMinute.Add(x, new LinkedList<WheelTask>());
wheelSecond.Add(x, new LinkedList<WheelTask>());
});
Enumerable.Range(0, k_Level1).ToList().ForEach(x =>
{
wheelMillisecond.Add(x, new LinkedList<WheelTask>());
});
说完轮的定义和初始化之后,回到Update()函数里面来。
每次获取当前时间
DateTime now = DateTime.Now;
通过当前时间的年份,获取年轮对应槽位的双链表yearLinkedList
LinkedList<WheelTask> yearLinkedList = wheelYear[now.Year];
while循环判断双链表yearLinkedList是否有元素,若有则拿出第一个来,放到月轮的某个槽位数上的链表里,这个某槽位数就是任务里面保存的dateTime所在的月份。
while (yearLinkedList.Any())
{
LinkedListNode<WheelTask> node = yearLinkedList.First;
yearLinkedList.RemoveFirst();
LinkedList<WheelTask> nextLinkedList = wheelMonth[node.Value.dateTime.Month];
nextLinkedList.AddLast(node);
}
同理,当前时间所在的月份,若月轮链表有元素,则将元素拿出来放到任务执行时间dateTime所在的日轮上。
LinkedList<WheelTask> monthLinkedList = wheelMonth[now.Month];
while (monthLinkedList.Any())
{
LinkedListNode<WheelTask> node = monthLinkedList.First;
monthLinkedList.RemoveFirst();
LinkedList<WheelTask> nextLinkedList = wheelDay[node.Value.dateTime.Day];
nextLinkedList.AddLast(node);
}
再同理,当前时间所在的日份,若日轮链表有元素,则将元素拿出来放到任务执行时间dateTime所在的时轮上。
LinkedList<WheelTask> dayLinkedList = wheelDay[now.Day];
while (dayLinkedList.Any())
{
LinkedListNode<WheelTask> node = dayLinkedList.First;
dayLinkedList.RemoveFirst();
LinkedList<WheelTask> nextLinkedList = wheelHour[node.Value.dateTime.Hour];
nextLinkedList.AddLast(node);
}
同理,时轮、分轮的调度也是一样的。
只有秒轮和毫秒轮的处理有些不一样。
前面提到了,毫秒轮只分了10份,因此:
假设执行任务时间dateTime的毫秒值是456,那么该任务应该推向毫秒轮的哪个槽位呢?
槽位数索引从0开始算起,应该推向索引为4的槽位上,如下代码的逻辑所示:
int x = k_Millisecond / k_Level1; //把1000毫秒分割成10等份,每份占100毫秒
LinkedList<WheelTask> secondLinkedList = wheelSecond[now.Second];
while (secondLinkedList.Any())
{
LinkedListNode<WheelTask> node = secondLinkedList.First;
secondLinkedList.RemoveFirst();
//当前毫秒值落在弟y等份,例如:当前毫秒50,则落在第0份上
int nextY = node.Value.dateTime.Millisecond / x;
LinkedList<WheelTask> nextLinkedList = wheelMillisecond[nextY];
nextLinkedList.AddLast(node);
}
有了这个秒轮的理解,那么毫秒轮的处理逻辑也不难理解了。
根据当前时间now的毫秒值,算出应该拿出哪个毫秒轮在处理。
处在毫秒轮的任务链表,都将会被执行掉。这个毫秒轮也就是前面提到的执行轮。
int x = k_Millisecond / k_Level1; //把1000毫秒分割成10等份,每份占100毫秒
int y = now.Millisecond / x; //当前毫秒值落在弟y等份,例如:当前毫秒50,则落在第0份上
LinkedList<WheelTask> millisecondLinkedList = wheelMillisecond[y];
while (millisecondLinkedList.Any())
{
foreach (WheelTask wheelTask in millisecondLinkedList)
{
if (DateTime.Now >= wheelTask.dateTime)
{
millisecondLinkedList.Remove(wheelTask);
wheelTask.Run();
//wheelTask.RunAsync();
if (wheelTask.CheckLoop())
{
AddTask(wheelTask);
}
break;
}
}
}
执行轮里面检测任务是否有循环,如果有,就重新添加这个任务。
在年轮上添加。
private void AddTask(WheelTask wheelTask)
{
wheelYear[wheelTask.dateTime.Year].AddLast(wheelTask);
}
既然有添加,那就有移除。做一个有手尾的人,不要让系统给你擦屁股。
private bool RemoveTask(Dictionary<int, LinkedList<WheelTask>> wheel, int id)
{
foreach (var item in wheel)
{
LinkedList<WheelTask> wheelTasks = item.Value;
foreach (var wheelTask in wheelTasks)
{
if (wheelTask.id == id)
{
wheelTasks.Remove(wheelTask);
return true;
}
}
}
return false;
}
最后,提供两个方法暴露给外界调用定时器。
1.添加定时器任务。
public int SetInterval(Action action, int id, int delay, int interval, int loopTimes)
{
lock(s_obj)
{
WheelTask wheelTask = new WheelTask(id, delay, interval, loopTimes, action);
AddTask(wheelTask);
return id;
}
}
2.清除定时器任务。
public void ClearInterval(int id)
{
lock(s_obj)
{
if (RemoveTask(wheelYear, id))
{
return;
}
if (RemoveTask(wheelMonth, id))
{
return;
}
if (RemoveTask(wheelDay, id))
{
return;
}
if (RemoveTask(wheelHour, id))
{
return;
}
if (RemoveTask(wheelMinute, id))
{
return;
}
if (RemoveTask(wheelSecond, id))
{
return;
}
if (RemoveTask(wheelMillisecond, id))
{
return;
}
}
}
在主程序里,就可以使用了。
public static void Test()
{
intervalId = timeWheel.SetInterval(Func3, 1, 1000, 2000, -1);
timeWheel.SetInterval(Func2_Clear, 2, 6000, 1000, 1);
}
public static void Func2_Clear()
{
timeWheel.ClearInterval(intervalId);
Console.WriteLine("定时执行了Func2_Clear " + DateTime.Now + " 清除了定时任务" + intervalId);
}
public static void Func3()
{
Console.WriteLine("定时执行了Func3 " + DateTime.Now);
}
程序输出结果如图:
最后的最后,奉上全部代码。
WheelTask.cs,TimeWheel.cs,主程序Program.cs
WheelTask类的全部代码。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TimeWheelProject
{
internal class WheelTask
{
/// <summary>任务唯一id </summary>
public int id;
/// <summary>延迟时间 </summary>
public int delay;
/// <summary>时间间隔 </summary>
public int interval;
/// <summary>循环次数 (-1则无限循环) </summary>
public int loopTimes;
/// <summary>执行时间 </summary>
public DateTime dateTime;
/// <summary>执行函数 </summary>
public Action action;
/// <summary>已循环的次数 </summary>
private int m_hadLoopTimes;
public WheelTask(int id, int delay, int interval, int loopTimes, Action action)
{
this.id = id;
this.delay = delay;
this.interval = interval;
this.loopTimes = loopTimes;
this.action = action;
m_hadLoopTimes = 0;
dateTime = DateTime.Now;
dateTime = dateTime.AddMilliseconds(this.delay);
}
public void Run()
{
action();
}
public async Task RunAsync()
{
await Task.Run(() =>
{
action();
});
}
public bool CheckLoop()
{
m_hadLoopTimes++;
if (loopTimes == -1)
{
dateTime = DateTime.Now.AddMilliseconds(interval);
return true;
}
else if(loopTimes > 0)
{
if (loopTimes > m_hadLoopTimes)
{
dateTime = DateTime.Now.AddMilliseconds(interval);
return true;
}
}
return false;
}
}
}
TimeWheel类的全部代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TimeWheelProject
{
internal class TimeWheel
{
private const int k_Millisecond = 1000;
private const int k_Level6 = 100; //先来个百年奋斗目标,没人会定超过100年的定时器吧?
private const int k_Level5 = 12; //1年即12个月 分割成12份
private const int k_Level4 = 31; //1个月即31天 分割成31份
private const int k_Level3 = 24; //1天即24小时 分割成24份
private const int k_Level2 = 60; //1小时和1分钟 都分割成60份
private const int k_Level1 = 10; //1秒即1000毫秒 分割成10份,每份100毫秒
private long m_temp = 0;
private static object s_obj = new object();
/// <summary>年轮 </summary>
private Dictionary<int, LinkedList<WheelTask>> wheelYear;
/// <summary>月轮 </summary>
private Dictionary<int, LinkedList<WheelTask>> wheelMonth;
/// <summary>日轮 </summary>
private Dictionary<int, LinkedList<WheelTask>> wheelDay;
/// <summary>时轮 </summary>
private Dictionary<int, LinkedList<WheelTask>> wheelHour;
/// <summary>分轮 </summary>
private Dictionary<int, LinkedList<WheelTask>> wheelMinute;
/// <summary>秒轮 </summary>
private Dictionary<int, LinkedList<WheelTask>> wheelSecond;
/// <summary>毫秒轮 </summary>
private Dictionary<int, LinkedList<WheelTask>> wheelMillisecond;
public TimeWheel()
{
wheelYear = new Dictionary<int, LinkedList<WheelTask>>();
wheelMonth = new Dictionary<int, LinkedList<WheelTask>>();
wheelDay = new Dictionary<int, LinkedList<WheelTask>>();
wheelHour = new Dictionary<int, LinkedList<WheelTask>>();
wheelMinute = new Dictionary<int, LinkedList<WheelTask>>();
wheelSecond = new Dictionary<int, LinkedList<WheelTask>>();
wheelMillisecond = new Dictionary<int, LinkedList<WheelTask>>();
//轮盘最大刻度,就从2023年开始到2123年吧 嘻嘻
Enumerable.Range(2023, 2023 + k_Level6).ToList().ForEach(x =>
{
wheelYear.Add(x, new LinkedList<WheelTask>());
});
Enumerable.Range(0, k_Level5).ToList().ForEach(x =>
{
wheelMonth.Add(x, new LinkedList<WheelTask>());
});
Enumerable.Range(0, k_Level4).ToList().ForEach(x =>
{
wheelDay.Add(x, new LinkedList<WheelTask>());
});
Enumerable.Range(0, k_Level3).ToList().ForEach(x =>
{
wheelHour.Add(x, new LinkedList<WheelTask>());
});
Enumerable.Range(0, k_Level2).ToList().ForEach(x =>
{
wheelMinute.Add(x, new LinkedList<WheelTask>());
wheelSecond.Add(x, new LinkedList<WheelTask>());
});
Enumerable.Range(0, k_Level1).ToList().ForEach(x =>
{
wheelMillisecond.Add(x, new LinkedList<WheelTask>());
});
}
public void Update()
{
lock (s_obj)
{
DateTime now = DateTime.Now;
if (now.Ticks - m_temp >= 10000000)
{
Console.WriteLine("当前时间 " + now);
m_temp = now.Ticks;
}
LinkedList<WheelTask> yearLinkedList = wheelYear[now.Year];
LinkedList<WheelTask> monthLinkedList = wheelMonth[now.Month];
LinkedList<WheelTask> dayLinkedList = wheelDay[now.Day];
LinkedList<WheelTask> hourLinkedList = wheelHour[now.Hour];
LinkedList<WheelTask> minuteLinkedList = wheelMinute[now.Minute];
LinkedList<WheelTask> secondLinkedList = wheelSecond[now.Second];
int x = k_Millisecond / k_Level1; //把1000毫秒分割成10等份,每份占100毫秒
int y = now.Millisecond / x; //当前毫秒值落在弟y等份,例如:当前毫秒50,则落在第0份上
LinkedList<WheelTask> millisecondLinkedList = wheelMillisecond[y];
while (yearLinkedList.Any())
{
LinkedListNode<WheelTask> node = yearLinkedList.First;
yearLinkedList.RemoveFirst();
LinkedList<WheelTask> nextLinkedList = wheelMonth[node.Value.dateTime.Month];
nextLinkedList.AddLast(node);
}
while (monthLinkedList.Any())
{
LinkedListNode<WheelTask> node = monthLinkedList.First;
monthLinkedList.RemoveFirst();
LinkedList<WheelTask> nextLinkedList = wheelDay[node.Value.dateTime.Day];
nextLinkedList.AddLast(node);
}
while (dayLinkedList.Any())
{
LinkedListNode<WheelTask> node = dayLinkedList.First;
dayLinkedList.RemoveFirst();
LinkedList<WheelTask> nextLinkedList = wheelHour[node.Value.dateTime.Hour];
nextLinkedList.AddLast(node);
}
while (hourLinkedList.Any())
{
LinkedListNode<WheelTask> node = hourLinkedList.First;
hourLinkedList.RemoveFirst();
LinkedList<WheelTask> nextLinkedList = wheelMinute[node.Value.dateTime.Minute];
nextLinkedList.AddLast(node);
}
while (minuteLinkedList.Any())
{
LinkedListNode<WheelTask> node = minuteLinkedList.First;
minuteLinkedList.RemoveFirst();
LinkedList<WheelTask> nextLinkedList = wheelSecond[node.Value.dateTime.Second];
nextLinkedList.AddLast(node);
}
while (secondLinkedList.Any())
{
LinkedListNode<WheelTask> node = secondLinkedList.First;
secondLinkedList.RemoveFirst();
//当前毫秒值落在弟y等份,例如:当前毫秒50,则落在第0份上
int nextY = node.Value.dateTime.Millisecond / x;
LinkedList<WheelTask> nextLinkedList = wheelMillisecond[nextY];
nextLinkedList.AddLast(node);
}
while (millisecondLinkedList.Any())
{
foreach (WheelTask wheelTask in millisecondLinkedList)
{
if (DateTime.Now >= wheelTask.dateTime)
{
millisecondLinkedList.Remove(wheelTask);
wheelTask.Run();
//wheelTask.RunAsync();
if (wheelTask.CheckLoop())
{
AddTask(wheelTask);
}
break;
}
}
}
}
}
private void AddTask(WheelTask wheelTask)
{
wheelYear[wheelTask.dateTime.Year].AddLast(wheelTask);
}
private bool RemoveTask(Dictionary<int, LinkedList<WheelTask>> wheel, int id)
{
foreach (var item in wheel)
{
LinkedList<WheelTask> wheelTasks = item.Value;
foreach (var wheelTask in wheelTasks)
{
if (wheelTask.id == id)
{
wheelTasks.Remove(wheelTask);
return true;
}
}
}
return false;
}
public int SetInterval(Action action, int id, int delay, int interval, int loopTimes)
{
lock(s_obj)
{
WheelTask wheelTask = new WheelTask(id, delay, interval, loopTimes, action);
AddTask(wheelTask);
return id;
}
}
public void ClearInterval(int id)
{
lock(s_obj)
{
if (RemoveTask(wheelYear, id))
{
return;
}
if (RemoveTask(wheelMonth, id))
{
return;
}
if (RemoveTask(wheelDay, id))
{
return;
}
if (RemoveTask(wheelHour, id))
{
return;
}
if (RemoveTask(wheelMinute, id))
{
return;
}
if (RemoveTask(wheelSecond, id))
{
return;
}
if (RemoveTask(wheelMillisecond, id))
{
return;
}
}
}
}
}
主程序入口Program 代码
using TimeWheelProject;
internal class Program
{
public static TimeWheel timeWheel;
private static Thread thread;
private static int intervalId;
private static void Main(string[] args)
{
timeWheel = new TimeWheel();
thread = new Thread(TimeWheelProgram);
thread.Start();
Test();
Console.ReadLine();
}
public static void TimeWheelProgram()
{
while (true)
{
timeWheel.Update();
}
}
public static void Test()
{
intervalId = timeWheel.SetInterval(Func3, 1, 1000, 2000, -1);
timeWheel.SetInterval(Func2_Clear, 2, 6000, 1000, 1);
}
public static void Func1()
{
int a = 0;
for (int i = 0; i < 100000; i++)
{
for (int j = 0; j < 100000; j++)
{
a++;
}
}
Console.WriteLine("定时执行了Func1 " + DateTime.Now + " a = " + a);
}
public static void Func2_Clear()
{
timeWheel.ClearInterval(intervalId);
Console.WriteLine("定时执行了Func2_Clear " + DateTime.Now + " 清除了定时任务" + intervalId);
}
public static void Func3()
{
Console.WriteLine("定时执行了Func3 " + DateTime.Now);
}
}
终于,结束了~~~