1.引言
當(dāng)執(zhí)行某些動作之后寡润,會期待反饋。最終要么是得到了結(jié)果舅柜,要么就是超時了悦穿。當(dāng)超時發(fā)生時,可能是期望得到通知业踢,或是希望能自動重試,等等礁扮。于是設(shè)計了一種通用的異步超時的處理機制知举,以期通過簡潔易理解的方式完成超時的處理過程。
2.對外接口設(shè)計
從使用的角度太伊,調(diào)用方期望的是“指定超時時長雇锡,時間到自動執(zhí)行指定過程”,由此可以得出外部的操作接口參數(shù)僚焦。從功能角度來看锰提,對于未超時的情況,需要提供在超時時長內(nèi)隨時清除超時任務(wù)的功能。
2.1操作接口
在這里立肘,我們把設(shè)計的機制稱里“超時任務(wù)運行器”边坤,從外部來看,其接口與功能結(jié)構(gòu)如下:
(1)添加超時任務(wù)谅年,帶上對象標(biāo)識用于回調(diào)時傳參茧痒,指定超時時長和超時回調(diào)方法即可把超時處理交給超時任務(wù)運行器。返回一個任務(wù)標(biāo)識融蹂,用于后續(xù)可刪除該超時任務(wù)旺订。
(2)刪除超時任務(wù),指定任務(wù)標(biāo)識即可刪除超燃。同時区拳,支持清除一個對象的所有超時任務(wù)。
2.2使用過程
發(fā)起異步操作的同時意乓,添加超時任務(wù)樱调,在異步操作成功時,刪除超時任務(wù)洽瞬。超時則運行器會自動執(zhí)行超時任務(wù)本涕。如下圖,灰色部分由運行器完成:
3.超時任務(wù)運行器設(shè)計與實現(xiàn)
首先伙窃,時長的精確粒度設(shè)定為秒菩颖,這表示超時最低可支持秒級(廢話)。設(shè)計的基本思路為:對于加入的超時任務(wù)为障,運行器建立清單晦闰,并以秒為單位對清單中的任務(wù)進行檢測,對于時間已經(jīng)到達或超過的將其移動至超時任務(wù)執(zhí)行隊列中鳍怨,由獨立的超時任務(wù)運行線程來執(zhí)行隊列中的任務(wù)呻右。這里,移動任務(wù)至執(zhí)行隊列的檢測者謂之“生產(chǎn)者”鞋喇,任務(wù)執(zhí)行線程謂之“消費者”声滥。
3.1基本結(jié)構(gòu)
運行器維護一個超時任務(wù)清單和一個執(zhí)行隊列,一個超時檢測者侦香,它使用定時器檢測任務(wù)是否超時并將超時的加入執(zhí)行隊列落塑,一個任務(wù)執(zhí)行者負責(zé)運行超時任務(wù)中的回調(diào)過程。
?
3.2數(shù)據(jù)結(jié)構(gòu)
超時任務(wù)信息罐韩,除了調(diào)用者傳遞的對象標(biāo)識憾赁、超時時長與回調(diào)方法,還包括其它運行過程中所需的屬性:任務(wù)標(biāo)識散吵、運行時間點龙考。同時為了在回調(diào)時能利用到一些對象相關(guān)的上下文信息蟆肆,再增加一個 context 屬性,它將作為參數(shù)傳遞給回調(diào)函數(shù)晦款,詳細信息看如下類定義:
/// <summary>
/// 超時回調(diào)的委托
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="objectKey"></param>
/// <param name="context"></param>
public delegate void TimeoutCallback<T>(T objectKey, String context);
/// <summary>
/// 超時任務(wù)信息
/// </summary>
/// <typeparam name="T"></typeparam>
public class TimeoutTask<T>
{
// 任務(wù)標(biāo)識(由超時任務(wù)運行器自動分配)
public long TaskId { get; set; }
// 對象標(biāo)識
public T ObjectKey { get; set; }
// 超時秒數(shù)
public int TimeoutSeconds { get; set; }
/// <summary>
/// 以秒為單位的 Tick 值炎功,由超時任務(wù)運行器根據(jù)當(dāng)前時間加上超時秒數(shù)計算設(shè)置
/// DateTime.Ticks 是以 10ns(10納秒) 為單位
/// 將其除以 10 單位為 ws(微秒),再除以 1000 為 ms(毫秒)柬赐,再除以 1000 為 s(秒)
/// 累計為 DateTime.Ticks / 10000000
/// </summary>
public long ExecuteSecondTicks { get; set; }
// 超時回調(diào)方法
public TimeoutCallback<T> Callback { get; set; }
/// <summary>
/// 用于保存一些回調(diào)時使用的上下文信息
/// </summary>
public String Context { get; set; }
}
3.3超時任務(wù)清單
任務(wù)清單亡问,在操作粒度上,可以以任務(wù)標(biāo)識為單位肛宋,也可以以對象標(biāo)識為單位州藕,因此,為了快速檢索酝陈。任務(wù)清單分兩種形式存儲床玻,一種以任務(wù)標(biāo)識為主鍵,另一種以對象標(biāo)識為主鍵沉帮,其結(jié)構(gòu)如下:
具體類型結(jié)構(gòu)定義如下锈死,_DictionaryLocker 有于同步加鎖,確保線程安全穆壕。
// 以 TaskId(任務(wù)標(biāo)識) 為 KEY 的任務(wù)清單字典
private Dictionary<long, TimeoutTask<T>> _TaskIdDictionary = new Dictionary<long, TimeoutTask<T>>();
// 以 ObjectId(任務(wù)相關(guān)對象標(biāo)識) 為 KEY 的任務(wù)字典待牵,因每個對象可以有多個超時任務(wù),所以為列表
private Dictionary<T, List<TimeoutTask<T>>> _TaskObjectKeyDictionary = new Dictionary<T, List<TimeoutTask<T>>>();
// 用于同步操作上述兩個清單字典喇勋,使得線程安全
private object _DictionaryLocker = new object();
3.4任務(wù)執(zhí)行隊列
一個普通的先進先出的隊列缨该,_RunLocker 用于線程安全加鎖。
// 已超時任務(wù)隊列川背,由任務(wù)運行線程逐個執(zhí)行
private Queue<TimeoutTask<T>> _TaskRunQueue = new Queue<TimeoutTask<T>>();
// 用來同步操作任務(wù)隊列贰拿,使得線程安全(生產(chǎn)者,消費者模式)
private object _RunLocker = new object();
3.5超時檢測者
以每秒進行一次檢測的粒度運行熄云,使用 System.Timers.Timer 非常合適膨更,它的職能是判斷運行時間到達與否決定是否將任務(wù)移至執(zhí)行隊列。
// 超時檢測者缴允,每秒掃描是否達到超時荚守,超時則加入超時任務(wù)隊列
private System.Timers.Timer _TimeoutChecker = new System.Timers.Timer();
// 超時檢測者
_TimeoutChecker.Interval = 1000;
_TimeoutChecker.Elapsed += new System.Timers.ElapsedEventHandler(CheckTimerTick);
_TimeoutChecker.Start();
/// <summary>
/// 超時任務(wù)檢測者
/// 對于,時間已經(jīng)超過了設(shè)定的超時時間的练般,加入超時任務(wù)執(zhí)行隊列
/// </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)
{
// 加入超時任務(wù)執(zhí)行隊列,并移除清單
lock (_RunLocker)
{
_TaskRunQueue.Enqueue(task);
RemoveTimeoutTask(task.TaskId);
}
// 有生產(chǎn)踢俄,則通知執(zhí)行線程(消費者)
_WaitHandle.Set();
}
}
}
}
3.6任務(wù)執(zhí)行者
執(zhí)行隊列中存在任務(wù)時就執(zhí)行,否則等待晴及。線程等待都办,這里使用了 EventWaitHandle,EventWaitHandle.WaitOne 等待,生產(chǎn)者使用 EventWaitHandle.Set 方法進行通知琳钉,配合起來有效地運行隊列中的任務(wù)势木。
// 超時任務(wù)執(zhí)行線程
private Thread _TaskRunThread;
// 用于同步操作任務(wù)隊列的線程信號(生產(chǎn)者,消費者通知作用)
private EventWaitHandle _WaitHandle = new AutoResetEvent(false);
// 用于退出執(zhí)行線程的一個標(biāo)識
private bool _Working = true;
/// <summary>
/// 超時任務(wù)執(zhí)行線程主體
/// </summary>
private void TaskRunning()
{
while (_Working)
{
TimeoutTask<T> task = null;
lock (_RunLocker)
{
if (_TaskRunQueue.Count > 0)
{
task = _TaskRunQueue.Dequeue();
}
}
// 存在超時任務(wù)執(zhí)行其回調(diào)
if (task != null)
{
task.Callback(task.ObjectKey, task.Context);
}
else
{
// 等待生產(chǎn)者通知
_WaitHandle.WaitOne();
}
}
}
3.7向外開放的接口
代碼如是說:
/// <summary>
/// 指定對象標(biāo)識歌懒,超時時長(秒為單位)啦桌,超時執(zhí)行回調(diào),加入到超時檢測字典中
/// </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)
{
// 以任務(wù)標(biāo)識為主鍵的任務(wù)清單
_TaskIdDictionary[taskId] = task;
// 以對象標(biāo)識為主鍵的任務(wù)清單
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>
/// 根據(jù)對象標(biāo)識移除超時任務(wù)設(shè)置
/// </summary>
/// <param name="objectKey"></param>
public void RemoveTimeoutTask(T objectKey)
{
lock (_DictionaryLocker)
{
if (_TaskObjectKeyDictionary.ContainsKey(objectKey))
{
// 在任務(wù)標(biāo)識為主鍵的清單中移除相應(yīng)的該對象的多個超時任務(wù)
foreach (var task in _TaskObjectKeyDictionary[objectKey])
{
_TaskIdDictionary.Remove(task.TaskId);
}
_TaskObjectKeyDictionary[objectKey].Clear();
}
}
}
/// <summary>
/// 根據(jù)任務(wù)標(biāo)識移除超時任務(wù)設(shè)置
/// </summary>
/// <param name="taskId"></param>
public void RemoveTimeoutTask(long taskId)
{
lock (_DictionaryLocker)
{
if (_TaskIdDictionary.ContainsKey(taskId))
{
var task = _TaskIdDictionary[taskId];
_TaskIdDictionary.Remove(taskId);
// 在對象標(biāo)識為主鍵的清單移除相應(yīng)的超時任務(wù)
_TaskObjectKeyDictionary[task.ObjectKey].Remove(task);
}
}
}
4.應(yīng)用示例
定義回調(diào)處理方法及皂,添加一個超時任務(wù)只需要指定簡單的參數(shù)即可甫男,如下示例,會按什么順序輸出什么呢验烧?
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();
}
}
運行結(jié)果:
5.小結(jié)
超時處理在異步通信中經(jīng)常會碰到板驳,實現(xiàn)超時處理的通用機制,能有效的復(fù)用代碼碍拆,提高效率若治。代碼仍然有很多優(yōu)化空間,如遍歷檢測超時是否有更合適的的方式等感混,歡迎探討端幼!
完整代碼請訪問: