前言

       在使用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类对其进行进阶应用。在服务器和客户端开发中,时间轮以及周期执行函数的应用十分广泛,使得我们的开发效率得以提高,学习和应用这个技术是非常有用的!

更多推荐