在很多.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);
}

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

  

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

  1. ORACLE调度之基于时间的调度(一)【weber出品】

    一.调度的概述 这里我看到一篇对调度的概述觉得描述的比我好,但仅限于概述部分,其他部分我觉得我讲的比他好,于是发生以下事情: ************************华丽的转载******** ...

  2. 制作一个简易计算器——基于Android Studio实现

    一个计算器Android程序的源码部分分为主干和细节两部分. 一.主干 1. 主干的构成 计算器的布局 事件(即计算器上的按钮.文本框)监听 实现计算 2. 详细解释 假设我们的项目名为Calcula ...

  3. 在项目管理中如何保持专注,分享一个轻量的时间管理工具【Flow Mac版 - 追踪你在Mac上的时间消耗】

    在项目管理和团队作业中,经常面临的问题就是时间管理和优先级管理发生问题,项目被delay,团队工作延后,无法达到预期目标. 这个仿佛是每个人都会遇到的问题,特别是现在这么多的内容软件来分散我们的注意力 ...

  4. go:基于时间轮定时器方案

    /* * http://blog.csdn.net/yueguanghaidao/article/details/46290539 * 修改内容:为定时器增加类型和参数属性,修改回调函数类型 */ p ...

  5. [C#]一个简易的、轻量级的方法并行执行线程辅助类

      一个简易的.轻量级的方法并行执行线程辅助类 在实际应用中,经常要让多个方法并行执行以节约运行时间,线程就是必不可少的了,而多线程的管理经常又是一件头疼的事情,比如方法并行执行异步的返回问题,方法并 ...

  6. Kafka中时间轮分析与Java实现

    在Kafka中应用了大量的延迟操作但在Kafka中 并没用使用JDK自带的Timer或是DelayQueue用于延迟操作,而是使用自己开发的DelayedOperationPurgatory组件用于管 ...

  7. Kafka解惑之时间轮 (TimingWheel)

    Kafka中存在大量的延迟操作,比如延迟生产.延迟拉取以及延迟删除等.Kafka并没有使用JDK自带的Timer或者DelayQueue来实现延迟的功能,而是基于时间轮自定义了一个用于实现延迟功能的定 ...

  8. 时间轮算法在Netty和Kafka中的应用,为什么不用Timer、延时线程池?

    大家好,我是yes. 最近看 Kafka 看到了时间轮算法,记得以前看 Netty 也看到过这玩意,没太过关注.今天就来看看时间轮到底是什么东西. 为什么要用时间轮算法来实现延迟操作? 延时操作 Ja ...

  9. [从源码学设计]蚂蚁金服SOFARegistry之时间轮的使用

    [从源码学设计]蚂蚁金服SOFARegistry之时间轮的使用 目录 [从源码学设计]蚂蚁金服SOFARegistry之时间轮的使用 0x00 摘要 0x01 业务领域 1.1 应用场景 0x02 定 ...

  10. .Net之时间轮算法(终极版)定时任务

    TimeWheelDemo 一个基于时间轮原理的定时器 对时间轮的理解 其实我是有一篇文章(.Net 之时间轮算法(终极版))针对时间轮的理论理解的,但是,我想,为啥我看完时间轮原理后,会采用这样的方 ...

随机推荐

  1. KMP模式匹配 学习笔记

    功能 能在线性时间内判断字符串\(A[1~N]\)是否为字符串\(B[1~M]\)的子串,并求出字符串\(A\)在字符串\(B\)中各次出现的位置. 实现 1.对字符串\(A\)进行自我"匹 ...

  2. Hbase创建表参数说明

    Hbase创建表操作及参数说明 1.创建命名空间 create_namespace 'test' 2.创建user表,列族:info create 'test:user', 'info' 3.查看表结 ...

  3. iframe的简单使用

    看人家写的真的是摸不着头脑.自己写.还是清楚 局部数据的刷新:可以使用ajax.这里只是简单的演示 只作:例子使用.简单演示页面跳转 a标签中target属性和iframe中的name对应.相当于将该 ...

  4. HTML基础知识(3)浮动、塌陷问题

    1.浮动 1.1 代码 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> & ...

  5. MyBatisPlus分页插件在SpringBoot中的使用

    文章目录 1.目录结构 2.新增配置 3.编写测试类 4.测试结果 5.数据库中的表 文件的创建: https://blog.csdn.net/weixin_43304253/article/deta ...

  6. 齐博x1模型里边钩子的创建与使用

    在模型里边的钩子创建与使用方法跟在控制器里边的钩子创建及使用方法是有所区别的在模型里边创建的钩子,你可以理解为执行一个函数,是无法调用模型里边的类的方法及属性的.比如系统文件\application\ ...

  7. jmeter中获取token和cookie

    ## 登录获取token 1.添加请求 1.1 输入接口中需要携带的参数的值 2.正则表达式提取器提取出值 3.输入token数据 "token":"(.+?)" ...

  8. 2.httprunner-yaml用例结构

    前言: httprunner3.x版本弱化了api层的概念 直接在testcase中写request请求 如果是单个请求,也可以直接写成一个testcase 每个testcase必须具有两个类属性:c ...

  9. 27.-Django发送邮件

    一.邮件相关协议-SMTP SMTP全称是Simple Mail Transfer Protocol,即简单邮件传输协议(25端口号) 它是由一组从源地址到目的地址传输邮件的规范,通过它来控制邮件的中 ...

  10. photoshop 2021 for mac安装教程,亲测可用!!!

    小编分享下photoshop cc 2021 for mac 安装教程,适配M1芯片,让大家完美使用ps2021,畅享所有新功能Adobe Photoshop2021(简称PS) 新版本主要增加了Ne ...