C#服务器——实现时间轮工具类(TimeWheel)
前言
在使用C#开发服务器时,定时执行的方法和延迟执行的函数十分常见,如果只是使用多线程和异步方法实现就会显得非常繁琐,增加开发时间。所以,开发一个时间轮工具类便显得十分重要了。本文将介绍时间轮的概念并实现时间轮工具类,并通过周期类(Schedule)对其进行拓展。
一、延时函数结构体的定义
字段解释:
- delayMs:表示该函数会延迟多少毫秒执行。
- action:具体执行的方法,用委托承载。
- taskNode:记录TimeTask在链表的位置,便于删除。
public struct TimeTask
{
public int delayMs;
public Action action;
public LinkedListNode<TimeTask>? taskNode;
public TimeTask(int delay, Action a)
{
delayMs = delay;
action = a;
}
}
二、时间轮类(TimeWheel)的实现
2.1 TimeWheel类中的字段
- count:时间轮层数,用于初始化。
- timeDelay:表示该时间轮的执行每层方法链表的时间间隔。
- slot:用链表记录时间轮中每层应该执行的方法。
- currentIndex:记录当前执行到的层数,用于遍历。
- slotLock:slot 的锁,避免多个地方添加和删除方法产生错误。
- isStoped:表示该时间轮是否停止运行。
//时间轮大小
public const int count = 64;
//执行时间间隔
public int timeDelay;
//方法链表
public readonly LinkedList<TimeTask>[] slot;
//当前执行到的方法
public int currentIndex = 0;
//锁
public readonly object slotLock = new object();
public bool isStoped = false;
2.2 TimeWheel的构造函数
构造函数接收一个int值,表示该时间轮的层数刷新时间间隔,接着初始化slot和每层的链表。
public TimeWheel(int delay = 10)
{
timeDelay = delay;
slot = new LinkedList<TimeTask>[count];
for (int i = 0; i < count; i++)
{
slot[i] = new LinkedList<TimeTask>();
}
}
2.3 TimeWheel的核心逻辑
Start方法是时间轮的开启接口,执行后将根据timeDelay的值循环执行Tick方法,通过刷新时间轮不断执行内部的方法。
在Tick函数中,首先会获取当前索引值中的所有任务,如果存在任务则存入内存中,遍历任务列表并执行任务,最后更新索引值。
public async Task Start()
{
isStoped = false;
while (!isStoped)
{
await Task.Delay(timeDelay);
Tick();
}
}
public void Tick()
{
var tasks = slot[currentIndex];
if (tasks.Count > 0)
{
List<TimeTask> tasks_temp = new List<TimeTask>(tasks);
tasks.Clear();
foreach (TimeTask task in tasks_temp)
{
task.action();
}
}
currentIndex = (currentIndex + 1) % count;
}
2.4 添加和删除任务
博主认为这个工具类的最大难点在于任务的添加逻辑,关键在于任务层数的计算,这里涉及时间轮的底层原理。这个工具类的底层原理是存在64个时间层(slot),每隔指定的时间(timeDelay)执行一个层。因此,想要在这个时间轮添加一个延迟delayTime执行的函数,这个任务就应该添加到经过delayTime时间后这个时间轮会执行到的层数,思考到这里,相信你已经有思路了,这个任务就应该添加到索引为(currentIndex + delayTime / timeDelay)的层中,当然不要忘记时间轮的最大层数限制,应该在算出这个索引后对count取模,完整公式为(currentIndex + delayTime / timeDelay) % count。
public void AddTask(int delayTime, Action action)
{
if (delayTime < timeDelay)
{
delayTime = timeDelay;
}
int slotIndex = (currentIndex + delayTime / timeDelay) % count;
TimeTask task = new TimeTask(delayTime, action);
lock (slotLock)
{
task.taskNode = slot[slotIndex].AddLast(task);
}
}
删除任务的逻辑较简单,如下:
public void RemoveTask(TimeTask task)
{
lock (slotLock)
{
task.taskNode?.List?.Remove(task.taskNode);
}
}
2.5 时间轮的停止
public void Stop()
{
isStoped = true;
}
三、时间轮的应用
了解了时间轮的原理后,应用它就十分简单了,只需要定义一个时间轮并开启,然后添加一个3000ms后执行的方法进行测试即可,是不是非常简单呢。
TimeWheel timeWheel = new TimeWheel(100);
_ = Task.Run(async () => {
await timeWheel.Start();
});
timeWheel.AddTask(3000, () =>
{
Log.Information("执行3秒后执行的方法");
});
四、进阶应用
目前,这个时间轮已经可以实现延迟若干秒执行一次指定任务,那如果想要间隔指定时间循环执行一个方法(类似Unity中的Update方法)该怎么实现呢?这里可以通过编写一个周期类(Scheduler)实现该功能。
可以看到,Scheduler类定义了一个TimeWheel类专门管理周期执行的方法,当我们想要添加一个间隔指定时间(delayMs)执行的方法(task)时,就可以通过调用其中的AddScheduler方法实现。AddScheduler中定义了一个新的函数RepeatTask,在执行原本的方法后又在timeWheel中添加RepeatTask方法,实现了递归调用的效果,这样便实现了循坏执行task方法的效果。
public class Scheduler : Singleton<Scheduler>
{
public TimeWheel _timeWheel;
private Scheduler()
{
_timeWheel = new();
Task.Run(() =>
{
_ = _timeWheel.Start();
});
}
public void AddScheduler(int delayMs, Action task)
{
void RepeatTask()
{
task();
_timeWheel.AddTask(delayMs, RepeatTask);
}
_timeWheel.AddTask(delayMs, RepeatTask);
}
}
五、完整代码展示
TimeWheel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Serilog;
namespace MMORPGServer.Time
{
public struct TimeTask
{
public int delayMs;
public Action action;
public LinkedListNode<TimeTask>? taskNode;
public TimeTask(int delay, Action a)
{
delayMs = delay;
action = a;
}
}
//时间轮类 用于执行延迟方法 最大可以接受 count * timeDelay 后执行的方法
public class TimeWheel
{
//时间轮大小
public const int count = 64;
//执行时间间隔
public int timeDelay;
//方法链表
public readonly LinkedList<TimeTask>[] slot;
//当前执行到的方法
public int currentIndex = 0;
//锁
public readonly object slotLock = new object();
public bool isStoped = false;
public TimeWheel(int delay = 10)
{
timeDelay = delay;
slot = new LinkedList<TimeTask>[count];
for (int i = 0; i < count; i++)
{
slot[i] = new LinkedList<TimeTask>();
}
}
public async Task Start()
{
isStoped = false;
while (!isStoped)
{
await Task.Delay(timeDelay);
Tick();
}
}
public void Tick()
{
var tasks = slot[currentIndex];
if (tasks.Count > 0)
{
List<TimeTask> tasks_temp = new List<TimeTask>(tasks);
tasks.Clear();
foreach (TimeTask task in tasks_temp)
{
task.action();
}
}
currentIndex = (currentIndex + 1) % count;
}
public void AddTask(int delayTime, Action action)
{
if (delayTime < timeDelay)
{
delayTime = timeDelay;
}
int slotIndex = (currentIndex + delayTime / timeDelay) % count;
TimeTask task = new TimeTask(delayTime, action);
lock (slotLock)
{
task.taskNode = slot[slotIndex].AddLast(task);
}
}
public void RemoveTask(TimeTask task)
{
lock (slotLock)
{
task.taskNode?.List?.Remove(task.taskNode);
}
}
public void Stop()
{
isStoped = true;
}
}
}
Scheduler.cs
using MMORPGServer.Tool;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MMORPGServer.Time
{
public class Scheduler : Singleton<Scheduler>
{
public TimeWheel _timeWheel;
private Scheduler()
{
_timeWheel = new();
Task.Run(() =>
{
_ = _timeWheel.Start();
});
}
public void AddScheduler(int delayMs, Action task)
{
void RepeatTask()
{
task();
_timeWheel.AddTask(delayMs, RepeatTask);
}
_timeWheel.AddTask(delayMs, RepeatTask);
}
}
}
六、总结
在这篇文章中,我们了解了时间轮(TimeWheel)的概念以及它的具体实现,并且学习到了通过Scheduler类对其进行进阶应用。在服务器和客户端开发中,时间轮以及周期执行函数的应用十分广泛,使得我们的开发效率得以提高,学习和应用这个技术是非常有用的!
更多推荐


所有评论(0)