首页 > 其他分享 >简易的延迟任务

简易的延迟任务

时间:2023-04-15 23:25:21浏览次数:48  
标签:secondTaskQueue 插槽 简易 任务 new secondSlot public 延迟

【c#】分享一个简易的基于时间轮调度的延迟任务实现

 

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

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

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

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

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

1 2 3 4 5 public class WheelTask<T> {     public T Data { getset; }     public Func<T, Task> Handle { getset; } }

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

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 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属性,用于当任务从分钟插槽弹出来时需要知道自己入队哪个秒插槽

1 2 3 4 5 6 public class WheelTask<T> {     ...     public int Second { getset; }     ... }

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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分钟的会按原逻辑直接入队到秒插槽中:

1 2 3 4 5 6 7 8 9 10 11 12 13 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以及任务执行的部分:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 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); }

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

标签:secondTaskQueue,插槽,简易,任务,new,secondSlot,public,延迟
From: https://www.cnblogs.com/Leo_wl/p/17322231.html

相关文章

  • 如何在Linux中加入cron任务
    从命令行中添加cron任务要添加cron任务,你可以使用称为crontab的命令行工具。输入下面的命令会创建一个以当前用户运行的新cron任务。1.$crontab-e如果你想要以其他用户运行cron任务,输入下面的命令。1.$sudocrontab-u-e你将会看见一个文本编辑窗口,这里你可以添加或者编辑cron......
  • js 异步任务执行顺序问题
    js是单线程的(非阻塞的),实现方法就是事件循环;分同步任务和异步任务;newPromise((resolve,reject)=>{resolve(1)console.log('log1')}).then(()=>{console.log('log2')})console.log('log3')setTimeout(()=>......
  • android studio 简易计算器制作
    只是记录一下代码,随意取用<?xmlversion="1.0"encoding="utf-8"?><LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width=&quo......
  • [PLC]三菱QD77MS16简易运动模块同步控制范例
    三菱QD77MS16简易运动模块同步控制范例三菱QD77MS16简易运动模块同步控制范例【详细讲解】QD77MS16是三菱新推出的一款简易型的运动控制模块。相对于普通定位模块具有控制轴数多(最多16轴),无电磁干扰(伺服光纤网络),支持同步控制,凸轮控制等优点。下面,海蓝机电将以实例来说明软件组态方......
  • 文盘Rust -- 用Tokio实现简易任务池
    作者:京东科技贾世闻Tokio无疑是Rust世界中最优秀的异步Runtime实现。非阻塞的特性带来了优异的性能,但是在实际的开发中我们往往需要在某些情况下阻塞任务来实现某些功能。我们看看下面的例子fnmain(){letmax_task=1;letrt=runtime::Builder::......
  • Python实现简易勒索病毒
    一、勒索病毒简单来说,勒索病毒就是通过加密受害者电脑的本地数据,向受害者勒索赎金的恶意软件。加密勒索软件的核心是加密算法,我自己实现的勒索病毒使用的就是安全度高破解难度大的RSA加密算法。RSA是一种非对称公钥加密算法,依赖于大质数分解难题,通过公钥无法轻易破解私钥。此......
  • AutoGPT自主完成复杂任务全程无需人类
    OpenAI的AndrejKarpathy都大力宣传,认为AutoGPT是prompt工程的下一个前沿。近日,AI界貌似出现了一种新的趋势:自主人工智能。这不是空穴来风,最近一个名为AutoGPT的研究开始走进大众视野。特斯拉前AI总监、刚刚回归OpenAI的AndrejKarpathy也为其大力......
  • VUE.JS和NODE.JS构建一个简易的前后端分离静态博客系统(三)
    Edit.vue<template><divid="edit"><ClassicHeader><templatev-slot:left><span>编辑随笔</span></template><templatev-slot:right><el-button@click="......
  • VUE.JS和NODE.JS构建一个简易的前后端分离静态博客系统(二)
    后台管理页面,需要配合NODE.JS搭建的EXPRESS服务器使用。main.jsimportVuefrom'vue'importAppfrom'./App.vue'importrouterfrom'./router'import{Button,Input,Form,Link,Divider,Upload,Dialog,Card,Popover,Messa......
  • Google SRE 定义了四个需要监控 延迟(Latency),流量(Traffic),错误(Errors)和饱和度(Saturati
    GoogleSRE定义了四个需要监控的关键指标。延迟(Latency),流量(Traffic),错误(Errors)和饱和度(Saturation)。正如google sre 所讨论的,如果您只能衡量服务的四个指标,请关注这四个指标。 延迟Latency延迟是服务处理传入请求和发送响应所用时间的度量。测量服务延迟有助于及早发现服......