C#基于时间轮调度实现延迟任务详解

作者:a1010 时间:2023-07-06 08:00:49 

在很多.net开发体系中开发者在面对调度作业需求的时候一般会选择三方开源成熟的作业调度框架来满足业务需求,比如Hangfire、Quartz.NET这样的框架。但是有些时候可能我们只是需要一个简易的延迟任务,这个时候引入这些框架就费力不讨好了。

最简单的粗暴的办法当然是:

Task.Run(async () =>
{
   //延迟xx毫秒
   await Task.Delay(time);
   //业务执行
});

当时作为一个开发者,有时候还是希望使用更优雅的、可复用的一体化方案,比如可以实现一个简易的时间轮来完成基于内存的非核心重要业务的延迟调度。什么是时间轮呢,其实就是一个环形数组,每一个数组有一个插槽代表对应时刻的任务,数组的值是一个任务队列,假设我们有一个基于60秒的延迟时间轮,也就是说我们的任务会在不超过60秒(超过的情况增加分钟插槽,下面会讲)的情况下执行,那么如何实现?下面我们将定义一段代码来实现这个简单的需求

话不多说,撸代码,首先我们需要定义一个时间轮的Model类用于承载我们的延迟任务和任务处理器。简单定义如下:

public class WheelTask<T>
{
   public T Data { get; set; }
   public Func<T, Task> Handle { get; set; }
}

定义很简单,就是一个入参T代表要执行的任务所需要的入参,然后就是任务的具体处理器Handle。接着我们来定义时间轮本轮的核心代码:

可以看到时间轮其实核心就两个东西,一个是毫秒计时器,一个是数组插槽,这里数组插槽我们使用了字典来实现,key值分别对应0到59秒。每一个插槽的value对应一个任务队列。当添加一个新任务的时候,输入需要延迟的秒数,就会将任务插入到延迟多少秒对应的插槽内,当计时器启动的时候,每一跳刚好1秒,那么就会对插槽计数+1,然后去寻找当前插槽是否有任务,有的话就会调用ExecuteTask执行该插槽下的所有任务。

public class TimeWheel<T>
{
   int secondSlot = 0;
   DateTime wheelTime { get { return new DateTime(1, 1, 1, 0, 0, secondSlot); } }
   Dictionary<int, ConcurrentQueue<WheelTask<T>>> secondTaskQueue;
   public void Start()
   {
       new Timer(Callback, null, 0, 1000);
       secondTaskQueue = new Dictionary<int, ConcurrentQueue<WheelTask<T>>>();
       Enumerable.Range(0, 60).ToList().ForEach(x =>
       {
           secondTaskQueue.Add(x, new ConcurrentQueue<WheelTask<T>>());
       });
   }
   public async Task AddTaskAsync(int second, T data, Func<T, Task> handler)
   {
       var handTime = wheelTime.AddSeconds(second);
       if (handTime.Second != wheelTime.Second)
           secondTaskQueue[handTime.Second].Enqueue(new WheelTask<T>(data, handler));
       else
           await handler(data);
   }
   async void Callback(object o)
   {
       if (secondSlot != 59)
           secondSlot++;
       else
       {
           secondSlot = 0;
       }
       if (secondTaskQueue[secondSlot].Any())
           await ExecuteTask();
   }
   async Task ExecuteTask()
   {
       if (secondTaskQueue[secondSlot].Any())
           while (secondTaskQueue[secondSlot].Any())
               if (secondTaskQueue[secondSlot].TryDequeue(out WheelTask<T> task))
                   await task.Handle(task.Data);
   }
}

接下来就是如果我需要大于60秒的情况如何处理呢。其实就是增加分钟插槽数组,举个例子我有一个任务需要2分40秒后执行,那么当我 插入到时间轮的时候我先插入到分钟插槽,当计时器每过去60秒,分钟插槽值+1,当分钟插槽对应有任务的时候就将这些任务从分钟插槽里弹出再入队到秒插槽中,这样一个任务会先进入插槽值=2(假设从0开始计算)的分钟插槽,计时器运行120秒后分钟值从0累加到2,2插槽的任务弹出到插槽值=40的秒插槽里,当计时器再运行40秒,刚好就可以执行这个延迟2分40秒的任务。话不多说,上代码:

首先我们将任务WheelTask增加一个Second属性,用于当任务从分钟插槽弹出来时需要知道自己入队哪个秒插槽

public class WheelTask<T>
{
   ...
   public int Second { get; set; }
   ...
}

接着我们再重新定义时间轮的逻辑增加分钟插槽值以及插槽队列的部分

public class TimeWheel<T>
{
   int minuteSlot, secondSlot = 0;
   DateTime wheelTime { get { return new DateTime(1, 1, 1, 0, minuteSlot, secondSlot); } }
   Dictionary<int, ConcurrentQueue<WheelTask<T>>>  minuteTaskQueue, secondTaskQueue;
   public void Start()
   {
       new Timer(Callback, null, 0, 1000);、
       minuteTaskQueue = new Dictionary<int, ConcurrentQueue<WheelTask<T>>>();
       secondTaskQueue = new Dictionary<int, ConcurrentQueue<WheelTask<T>>>();
       Enumerable.Range(0, 60).ToList().ForEach(x =>
       {
           minuteTaskQueue.Add(x, new ConcurrentQueue<WheelTask<T>>());
           secondTaskQueue.Add(x, new ConcurrentQueue<WheelTask<T>>());
       });
   }
   ...
}

同样的在添加任务的AddTaskAsync函数中我们需要增加分钟,代码改为这样,当大于1分钟的任务会入队到分钟插槽中,小于1分钟的会按原逻辑直接入队到秒插槽中:

public async Task AddTaskAsync(int minute, int second, T data, Func<T, Task> handler)
{
   var handTime = wheelTime.AddMinutes(minute).AddSeconds(second);
       if (handTime.Minute != wheelTime.Minute)
           minuteTaskQueue[handTime.Minute].Enqueue(new WheelTask<T>(handTime.Second, data, handler));
       else
       {
           if (handTime.Second != wheelTime.Second)
               secondTaskQueue[handTime.Second].Enqueue(new WheelTask<T>(data, handler));
           else
               await handler(data);
       }
}

最后的部分就是计时器的callback以及任务执行的部分:

async void Callback(object o)
{
   bool minuteExecuteTask = false;
   if (secondSlot != 59)
       secondSlot++;
   else
   {
       secondSlot = 0;
       minuteExecuteTask = true;
       if (minuteSlot != 59)
           minuteSlot++;
       else
       {
           minuteSlot = 0;
       }
   }
   if (minuteExecuteTask || secondTaskQueue[secondSlot].Any())
       await ExecuteTask(minuteExecuteTask);
}
async Task ExecuteTask(bool minuteExecuteTask)
{
   if (minuteExecuteTask)
       while (minuteTaskQueue[minuteSlot].Any())
           if (minuteTaskQueue[minuteSlot].TryDequeue(out WheelTask<T> task))
               secondTaskQueue[task.Second].Enqueue(task);
   if (secondTaskQueue[secondSlot].Any())
       while (secondTaskQueue[secondSlot].Any())
           if (secondTaskQueue[secondSlot].TryDequeue(out WheelTask<T> task))
               await task.Handle(task.Data);
}

基本上基于分钟+秒的时间轮延迟任务核心功能就这些了,聪明的你一定知道如何扩展增加小时,天,月份甚至年份的时间轮了。虽然从代码逻辑上可以实现,但是大部分情况下我们使用时间轮仅仅是完成一些内存易失性的非核心的任务延迟调度,实现天,周,月年意义不是很大。所以基本上到小时就差不多了。再多就上作业系统来调度吧。

来源:https://www.cnblogs.com/gmmy/p/17015538.html

标签:C#,时间轮调度,延迟
0
投稿

猜你喜欢

  • Spring实现动态切换多数据源的解决方案

    2023-05-21 13:07:15
  • c#代码生成URL地址的示例

    2022-02-17 09:44:08
  • unity3d发布apk在android虚拟机中运行的详细步骤(unity3d导出android apk)

    2022-11-09 16:18:56
  • java中ZXing 生成、解析二维码图片的小示例

    2022-07-24 11:50:39
  • C#实现Menu和ContextMenu自定义风格及contextMenu自定义

    2022-01-03 04:09:30
  • java实现简易飞机大战

    2022-08-27 12:36:07
  • Android自定义EditText实现淘宝登录功能

    2023-04-20 01:33:04
  • 基于WPF实现简单放大镜效果

    2022-02-15 23:19:12
  • javaWeb 四大域对象详细介绍

    2021-08-16 14:04:01
  • 教你如何编写简单的网络爬虫

    2023-06-01 08:58:30
  • SpringMVC接收多个对象的4种方法

    2023-11-23 06:24:18
  • Android开发中ImageLoder进行图片加载和缓存

    2023-08-18 10:14:30
  • C#实现程序开机启动的方法

    2023-09-23 00:55:20
  • java实现客房管理系统

    2022-10-15 23:56:33
  • Java源码解析之接口List

    2022-06-13 08:46:44
  • Android 实现定时任务的过程详解

    2023-06-14 02:28:33
  • C#深拷贝方法探究及性能比较(多种深拷贝)

    2022-08-30 18:17:02
  • Java线程创建静态代理模式代码实例

    2021-11-17 18:37:23
  • java递归设置层级菜单的实现

    2023-03-05 14:14:57
  • Java实体类不要使用基本类型的知识点总结

    2023-02-21 10:04:49
  • asp之家 软件编程 m.aspxhome.com