一种极简的异步超时处理机制设计与实现(C#版)
1.引言
当执行某些动作之后,会期待反馈。最终要么是得到了结果,要么就是超时了。当超时发生时,可能是期望得到通知,或是希望能自动重试,等等。于是设计了一种通用的异步超时的处理机制,以期通过简洁易理解的方式完成超时的处理过程。
2.对外接口设计
从使用的角度,调用方期望的是“指定超时时长,时间到自动执行指定过程”,由此可以得出外部的操作接口参数。从功能角度来看,对于未超时的情况,需要提供在超时时长内随时清除超时任务的功能。
2.1操作接口
在这里,我们把设计的机制称里“超时任务运行器”,从外部来看,其接口与功能结构如下:
(1)添加超时任务,带上对象标识用于回调时传参,指定超时时长和超时回调方法即可把超时处理交给超时任务运行器。返回一个任务标识,用于后续可删除该超时任务。
(2)删除超时任务,指定任务标识即可删除。同时,支持清除一个对象的所有超时任务。
2.2使用过程
发起异步操作的同时,添加超时任务,在异步操作成功时,删除超时任务。超时则运行器会自动执行超时任务。如下图,灰色部分由运行器完成:
3.超时任务运行器设计与实现
首先,时长的精确粒度设定为秒,这表示超时最低可支持秒级(废话)。设计的基本思路为:对于加入的超时任务,运行器建立清单,并以秒为单位对清单中的任务进行检测,对于时间已经到达或超过的将其移动至超时任务执行队列中,由独立的超时任务运行线程来执行队列中的任务。这里,移动任务至执行队列的检测者谓之“生产者”,任务执行线程谓之“消费者”。
3.1基本结构
运行器维护一个超时任务清单和一个执行队列,一个超时检测者,它使用定时器检测任务是否超时并将超时的加入执行队列,一个任务执行者负责运行超时任务中的回调过程。
3.2数据结构
超时任务信息,除了调用者传递的对象标识、超时时长与回调方法,还包括其它运行过程中所需的属性:任务标识、运行时间点。同时为了在回调时能利用到一些对象相关的上下文信息,再增加一个 context 属性,它将作为参数传递给回调函数,详细信息看如下类定义:
/// <summary>
/// 超时回调的委托
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="objectKey"></param>
/// <param name="context"></param>
public delegate void TimeoutCallback<T>(T objectKey, String context);
/// <summary>
/// 超时任务信息
/// </summary>
/// <typeparam name="T"></typeparam>
public class TimeoutTask<T>
{
// 任务标识(由超时任务运行器自动分配)
public long TaskId { get; set; }
// 对象标识
public T ObjectKey { get; set; }
// 超时秒数
public int TimeoutSeconds { get; set; }
/// <summary>
/// 以秒为单位的 Tick 值,由超时任务运行器根据当前时间加上超时秒数计算设置
/// DateTime.Ticks 是以 10ns(10纳秒) 为单位
/// 将其除以 10 单位为 ws(微秒),再除以 1000 为 ms(毫秒),再除以 1000 为 s(秒)
/// 累计为 DateTime.Ticks / 10000000
/// </summary>
public long ExecuteSecondTicks { get; set; }
// 超时回调方法
public TimeoutCallback<T> Callback { get; set; }
/// <summary>
/// 用于保存一些回调时使用的上下文信息
/// </summary>
public String Context { get; set; }
}
3.3超时任务清单
任务清单,在操作粒度上,可以以任务标识为单位,也可以以对象标识为单位,因此,为了快速检索。任务清单分两种形式存储,一种以任务标识为主键,另一种以对象标识为主键,其结构如下:
具体类型结构定义如下,_DictionaryLocker 有于同步加锁,确保线程安全。
// 以 TaskId(任务标识) 为 KEY 的任务清单字典
private Dictionary<long, TimeoutTask<T>> _TaskIdDictionary = new Dictionary<long, TimeoutTask<T>>();
// 以 ObjectId(任务相关对象标识) 为 KEY 的任务字典,因每个对象可以有多个超时任务,所以为列表
private Dictionary<T, List<TimeoutTask<T>>> _TaskObjectKeyDictionary = new Dictionary<T, List<TimeoutTask<T>>>();
// 用于同步操作上述两个清单字典,使得线程安全
private object _DictionaryLocker = new object();
3.4任务执行队列
一个普通的先进先出的队列,_RunLocker 用于线程安全加锁。
// 已超时任务队列,由任务运行线程逐个执行
private Queue<TimeoutTask<T>> _TaskRunQueue = new Queue<TimeoutTask<T>>();
// 用来同步操作任务队列,使得线程安全(生产者,消费者模式)
private object _RunLocker = new object();
3.5超时检测者
以每秒进行一次检测的粒度运行,使用 System.Timers.Timer 非常合适,它的职能是判断运行时间到达与否决定是否将任务移至执行队列。
// 超时检测者,每秒扫描是否达到超时,超时则加入超时任务队列
private System.Timers.Timer _TimeoutChecker = new System.Timers.Timer();
// 超时检测者
_TimeoutChecker.Interval = 1000;
_TimeoutChecker.Elapsed += new System.Timers.ElapsedEventHandler(CheckTimerTick);
_TimeoutChecker.Start();
/// <summary>
/// 超时任务检测者
/// 对于,时间已经超过了设定的超时时间的,加入超时任务执行队列
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void CheckTimerTick(object sender, System.Timers.ElapsedEventArgs e)
{
long secondTicks = DateTime.Now.Ticks / 10000000;
// 遍历,把时间已到达超过超时时间的找出来
lock (_DictionaryLocker)
{
foreach (var key in _TaskIdDictionary.Keys.ToList())
{
var task = _TaskIdDictionary[key];
if (_TaskIdDictionary[key].ExecuteSecondTicks <= secondTicks)
{
// 加入超时任务执行队列,并移除清单
lock (_RunLocker)
{
_TaskRunQueue.Enqueue(task);
RemoveTimeoutTask(task.TaskId);
}
// 有生产,则通知执行线程(消费者)
_WaitHandle.Set();
}
}
}
}
3.6任务执行者
执行队列中存在任务时就执行,否则等待。线程等待,这里使用了 EventWaitHandle,EventWaitHandle.WaitOne 等待,生产者使用 EventWaitHandle.Set 方法进行通知,配合起来有效地运行队列中的任务。
// 超时任务执行线程
private Thread _TaskRunThread;
// 用于同步操作任务队列的线程信号(生产者,消费者通知作用)
private EventWaitHandle _WaitHandle = new AutoResetEvent(false);
// 用于退出执行线程的一个标识
private bool _Working = true;
/// <summary>
/// 超时任务执行线程主体
/// </summary>
private void TaskRunning()
{
while (_Working)
{
TimeoutTask<T> task = null;
lock (_RunLocker)
{
if (_TaskRunQueue.Count > 0)
{
task = _TaskRunQueue.Dequeue();
}
}
// 存在超时任务执行其回调
if (task != null)
{
task.Callback(task.ObjectKey, task.Context);
}
else
{
// 等待生产者通知
_WaitHandle.WaitOne();
}
}
}
3.7向外开放的接口
代码如是说:
/// <summary>
/// 指定对象标识,超时时长(秒为单位),超时执行回调,加入到超时检测字典中
/// </summary>
/// <param name="objectKey"></param>
/// <param name="timeoutSeconds"></param>
/// <param name="callback"></param>
/// <param name="context"></param>
/// <returns></returns>
public long AddTimeoutTask(T objectKey, int timeoutSeconds, TimeoutCallback<T> callback, String context)
{
TimeoutTask<T> task = new TimeoutTask<T>();
task.ObjectKey = objectKey;
task.TimeoutSeconds = timeoutSeconds;
task.Callback = callback;
long taskId = GetNextTaskId();
task.TaskId = taskId;
task.ExecuteSecondTicks = DateTime.Now.Ticks / 10000000 + timeoutSeconds;
task.Context = context;
lock (_DictionaryLocker)
{
// 以任务标识为主键的任务清单
_TaskIdDictionary[taskId] = task;
// 以对象标识为主键的任务清单
if (_TaskObjectKeyDictionary.ContainsKey(objectKey))
{
_TaskObjectKeyDictionary[objectKey].Add(task);
}
else
{
List<TimeoutTask<T>> list = new List<TimeoutTask<T>>();
list.Add(task);
_TaskObjectKeyDictionary[objectKey] = list;
}
}
return taskId;
}
/// <summary>
/// 根据对象标识移除超时任务设置
/// </summary>
/// <param name="objectKey"></param>
public void RemoveTimeoutTask(T objectKey)
{
lock (_DictionaryLocker)
{
if (_TaskObjectKeyDictionary.ContainsKey(objectKey))
{
// 在任务标识为主键的清单中移除相应的该对象的多个超时任务
foreach (var task in _TaskObjectKeyDictionary[objectKey])
{
_TaskIdDictionary.Remove(task.TaskId);
}
_TaskObjectKeyDictionary[objectKey].Clear();
}
}
}
/// <summary>
/// 根据任务标识移除超时任务设置
/// </summary>
/// <param name="taskId"></param>
public void RemoveTimeoutTask(long taskId)
{
lock (_DictionaryLocker)
{
if (_TaskIdDictionary.ContainsKey(taskId))
{
var task = _TaskIdDictionary[taskId];
_TaskIdDictionary.Remove(taskId);
// 在对象标识为主键的清单移除相应的超时任务
_TaskObjectKeyDictionary[task.ObjectKey].Remove(task);
}
}
}
4.应用示例
定义回调处理方法,添加一个超时任务只需要指定简单的参数即可,如下示例,会按什么顺序输出什么呢?
class Program
{
static void Main(string[] args)
{
TS.Task.TimeoutTaskRunner<string> runner = new TS.Task.TimeoutTaskRunner<string>();
TS.Task.TimeoutCallback<string> callback = (string key, string context) =>
{
Console.WriteLine(key + " is timeout.");
};
runner.AddTimeoutTask("a", 4, callback, null);
runner.AddTimeoutTask("b", 3, callback, null);
runner.AddTimeoutTask("c", 2, callback, null);
Console.ReadKey();
runner.Dispose();
}
}
运行结果:
5.小结
超时处理在异步通信中经常会碰到,实现超时处理的通用机制,能有效的复用代码,提高效率。代码仍然有很多优化空间,如遍历检测超时是否有更合适的的方式等,欢迎探讨!
完整代码请访问:
https://github.com/triplestudio/TimeoutTask
一种极简的异步超时处理机制设计与实现(C#版)的更多相关文章
- C++服务器设计(四):超时管理机制设计
前四章介绍了系统层的设计,从这一章开始进入服务层的设计. 连接断开 在常见的服务器场景中,客户端断开连接的方式为被动关闭.即作为客户端请求完服务器的服务后,选择主动关闭同服务器的连接.在服务器的角度看 ...
- .NET Core实战项目之CMS 第八章 设计篇-内容管理极简设计全过程
写在前面 上一篇文章中我带着大家进行了权限部分的极简设计,也仅仅是一个基本的权限设计.不过你完全可以基于这套权限系统设计你的更复杂的权限系统,当然更复杂的权限系统要根据你的业务来进行,因为任何脱离实际 ...
- 极简Vue的异步组件函数
export default new Router({ routes: [ { path: '/live', name: 'live', component: () => import('@/v ...
- 【转】手摸手,带你用vue撸后台 系列四(vueAdmin 一个极简的后台基础模板)
前言 做这个 vueAdmin-template 的主要原因是: vue-element-admin 这个项目的初衷是一个vue的管理后台集成方案,把平时用到的一些组件或者经验分享给大家,同时它也在不 ...
- HTML5 极简的JS函数
页面初始化 mui框架将很多功能配置都集中在mui.init方法中,要使用某项功能,只需要在mui.init方法中完成对应参数配置即可,目前支持在mui.init方法中配置的功能包括:创建子页面.关闭 ...
- Nginx 极简教程(快速入门)
作者:dunwu github.com/dunwu/nginx-tutorial 推荐阅读(点击即可跳转阅读) 1. SpringBoot内容聚合 2. 面试题内容聚合 3. 设计模式内容聚合 4. ...
- 【股票盯盘软件】01_程序员炒股之开发一款极简风格的股票盯盘软件StockDog_V1.0.0.1
1.前言 话说最近一段时间受疫情的影响,股市各种妖魔横行.本人作为一个入股市不满三年的小韭菜,就有幸见证了好几次历史,也是满心惊喜,就权当是接受资本市场的再教育了吧. 小韭菜的炒股方法其实很简单,这两 ...
- nginx极简教程
Nginx 极简教程 本项目是一个 Nginx 极简教程,目的在于帮助新手快速入门 Nginx. examples 目录中的示例模拟了工作中的一些常用实战场景,并且都可以通过脚本一键式启动,让您可以快 ...
- 极简 Node.js 入门 - 2.2 事件
极简 Node.js 入门系列教程:https://www.yuque.com/sunluyong/node 本文更佳阅读体验:https://www.yuque.com/sunluyong/node ...
随机推荐
- MySQL之索引(一)
创建高性能索引 索引是存储引擎用于快速找到记录的一种数据结构.这是索引的基本功能.索引对于良好的性能非常关键.尤其是当表中的数据量越来越大时,索引对性能的影响愈发重要.在数据量较小且负载较低时,不恰当 ...
- spark的flatMap和map区别
map()是将函数用于RDD中的每个元素,将返回值构成新的RDD. flatmap()是将函数应用于RDD中的每个元素,将返回的迭代器的所有内容构成新的RDD,这样就得到了一个由各列表中的元素组成的R ...
- mysql之面试问题总结
问题1.char 与varchar的区别? varchar是变长而char的长度是固定的.如果你的内容是固定的大小,char性能更好. char[4] 与varchar[4] 存储字母a a占一个 ...
- XX公司在线笔试题编程题之一
题目: #include <iostream> #include <vector> #include <string> #include <list> ...
- IOS开发---菜鸟学习之路--(五)-MacBook购买前后感想
前几天刚入手了一台MACBOOK AIR 13寸 13版的 这几天使用过来个人感觉还是非常不错的. 这几天每天晚上都抱着她玩到十一.二点. 今天晚上突然想起来好久没续写博客了.就连忙开始码字了. 此章 ...
- jmeter全局变量配置:将token运用到全局(跨线程组使用变量)
请注意元器件的执行顺序: 请将提取token的配置原件放在设置全局变量的配置元器件前面(本来是一个超级马虎的人,真是俗称“方脑壳”啊) 1.获取登录后的token(提取可以用json path Ext ...
- Windows系统中 JDK安装及环境配置
需要安装jdk的第一步就是先去官网下载好JDK,选择需要的版本. Windows系统 1.将下载好的压缩包解压,点击解压得到的jdk执行文件开始安装.在安装过程中会弹出两个安装,一个是jdk,一个是j ...
- 内置函数,重要的四个reduce,map,lambda,filter
#filter过滤器#filter(函数,列表)#把列表里的元素序列化,然后在函数中过滤# str=["a","b","c","d ...
- mysql数据库增、删、改、查等基本命令
测试环境:windows7 64位 mysql.exe.Navicat Lite for MySQL.mysql 5.0.18 mysql数据库的基本结构: 数据库(database)包含多个表(ta ...
- [python][django学习篇][15]博客侧栏--自定义模板标签
我们的博客侧边栏有四项内容:最新文章.归档.分类和标签云. 这些内容相对比较固定,且在各个页面都会显示,如果像文章列表或者文章详情一样,从视图函数中获取然后传递给模板,则每个页面对应的视图函数里都要写 ...