【c#】分享一个简易的基于时间轮调度的延迟任务实现
在很多.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#】分享一个简易的基于时间轮调度的延迟任务实现的更多相关文章
- ORACLE调度之基于时间的调度(一)【weber出品】
一.调度的概述 这里我看到一篇对调度的概述觉得描述的比我好,但仅限于概述部分,其他部分我觉得我讲的比他好,于是发生以下事情: ************************华丽的转载******** ...
- 制作一个简易计算器——基于Android Studio实现
一个计算器Android程序的源码部分分为主干和细节两部分. 一.主干 1. 主干的构成 计算器的布局 事件(即计算器上的按钮.文本框)监听 实现计算 2. 详细解释 假设我们的项目名为Calcula ...
- 在项目管理中如何保持专注,分享一个轻量的时间管理工具【Flow Mac版 - 追踪你在Mac上的时间消耗】
在项目管理和团队作业中,经常面临的问题就是时间管理和优先级管理发生问题,项目被delay,团队工作延后,无法达到预期目标. 这个仿佛是每个人都会遇到的问题,特别是现在这么多的内容软件来分散我们的注意力 ...
- go:基于时间轮定时器方案
/* * http://blog.csdn.net/yueguanghaidao/article/details/46290539 * 修改内容:为定时器增加类型和参数属性,修改回调函数类型 */ p ...
- [C#]一个简易的、轻量级的方法并行执行线程辅助类
一个简易的.轻量级的方法并行执行线程辅助类 在实际应用中,经常要让多个方法并行执行以节约运行时间,线程就是必不可少的了,而多线程的管理经常又是一件头疼的事情,比如方法并行执行异步的返回问题,方法并 ...
- Kafka中时间轮分析与Java实现
在Kafka中应用了大量的延迟操作但在Kafka中 并没用使用JDK自带的Timer或是DelayQueue用于延迟操作,而是使用自己开发的DelayedOperationPurgatory组件用于管 ...
- Kafka解惑之时间轮 (TimingWheel)
Kafka中存在大量的延迟操作,比如延迟生产.延迟拉取以及延迟删除等.Kafka并没有使用JDK自带的Timer或者DelayQueue来实现延迟的功能,而是基于时间轮自定义了一个用于实现延迟功能的定 ...
- 时间轮算法在Netty和Kafka中的应用,为什么不用Timer、延时线程池?
大家好,我是yes. 最近看 Kafka 看到了时间轮算法,记得以前看 Netty 也看到过这玩意,没太过关注.今天就来看看时间轮到底是什么东西. 为什么要用时间轮算法来实现延迟操作? 延时操作 Ja ...
- [从源码学设计]蚂蚁金服SOFARegistry之时间轮的使用
[从源码学设计]蚂蚁金服SOFARegistry之时间轮的使用 目录 [从源码学设计]蚂蚁金服SOFARegistry之时间轮的使用 0x00 摘要 0x01 业务领域 1.1 应用场景 0x02 定 ...
- .Net之时间轮算法(终极版)定时任务
TimeWheelDemo 一个基于时间轮原理的定时器 对时间轮的理解 其实我是有一篇文章(.Net 之时间轮算法(终极版))针对时间轮的理论理解的,但是,我想,为啥我看完时间轮原理后,会采用这样的方 ...
随机推荐
- R及R Studio下载安装教程(超详细)
R 语言是为数学研究工作者设计的一种数学编程语言,主要用于统计分析.绘图.数据挖掘. 如果你是一个计算机程序的初学者并且急切地想了解计算机的通用编程,R 语言不是一个很理想的选择,可以选择 Pytho ...
- doecker---制作DockerFile并上传Hub
一.DockerFile基础知识 FROM #基础镜像,一切从这里开始构建 MAINTAINER #镜像是谁写的,姓名+邮箱 RUN #镜像构建的时候需要运行的命令 ADD #添加内容,步骤,tomc ...
- 网页计时访问器application对象
网页计时访问器 application对象代表Web应用本身,整个Web共享一个application对象.开始于服务器打开结束与服务器关闭.在不同的浏览器打开j计时器仍然有作用.除非把服务器关闭 & ...
- 齐博x1标签实例:调用多个圈子同时调用贴子
下面讲解,在首页,如何调用圈子的同时也调用他们相关的贴子. 单单调用圈子,就像调用文章一样,很多人都能轻松实现,比如下面的代码 {qb:tag name="xxx" type=&q ...
- el-cascader组件根据最后一级向上找到父级并设置默认值
vue + elementUI项目中,el-cascader级联选择器使用频率非常高,一些基本使用方法可以参考elementUI官方文档,本文主要研究当接口只返回最后一级id时,如何向上找出所有父级数 ...
- css文字单行/多行超出显示省略号...
css文字单行/多行超出显示省略号... 项目里写css样式我们经常会遇到将文字超出显示省略号的情况,记录一下以后能用到. 单行超出 .oneline { width:300upx; /*宽度一定要设 ...
- 恭喜磊哥喜提n+1
昨天下午两点多磊哥突然喊我下楼,第一反应是"这孙子,抽烟就直说,还说个事,你以外你是吉祥村大姐啊". 心里骂完以后我慢慢悠悠下楼了,见他在打电话我先默默点上一支,准备待他结束以后对 ...
- python模拟登录获取网站cookie
因工作的需要需要使用某第三方网站页面的功能,但其网站未提供API,只有登录用户可使用该功能. 之前试过php使用snoopy获取set-cookie内容项进行手动拼装cookie,使用一段时间后发现网 ...
- python 队列(QUEUE)
QUEUE python中多线程编程的数据结构 基本FIFO队列 class Queue.Queue(maxsize=0) 先进先出,maxsize为队列中能存放的数据个数上限. import Que ...
- Pytest学习
pytest简介 pytest是python的一种单元测试框架,与python自带的unittest测试框架类似,但是比unittest框架使用起来更简洁,效率更高. 使用介绍 编写规则 测试函数以t ...