1.引言
當執行某些動作之後,會期待反饋。最終要麼是得到了結果,要麼就是超時了。當超時發生時,可能是期望得到通知,或是希望能自動重試,等等。於是設計了一種通用的非同步超時的處理機制,以期通過簡潔易理解的方式完成超時的處理過程。
2.對外介面設計
從使用的角度,呼叫方期望的是“指定超時時長,時間到自動執行指定過程”,由此可以得出外部的操作介面引數。從功能角度來看,對於未超時的情況,需要提供在超時時長內隨時清除超時任務的功能。
2.1操作介面
在這裡,我們把設計的機制稱裡“超時任務執行器”,從外部來看,其介面與功能結構如下:
(1)新增超時任務,帶上物件標識用於回撥時傳參,指定超時時長和超時回撥方法即可把超時處理交給超時任務執行器。返回一個任務標識,用於後續可刪除該超時任務。
(2)刪除超時任務,指定任務標識即可刪除。同時,支援清除一個物件的所有超時任務。
2.2使用過程
發起非同步操作的同時,新增超時任務,在非同步操作成功時,刪除超時任務。超時則執行器會自動執行超時任務。如下圖,灰色部分由執行器完成:
3.超時任務執行器設計與實現
首先,時長的精確粒度設定為秒,這表示超時最低可支援秒級(廢話)。設計的基本思路為:對於加入的超時任務,執行器建立清單,並以秒為單位對清單中的任務進行檢測,對於時間已經到達或超過的將其移動至超時任務執行佇列中,由獨立的超時任務執行執行緒來執行佇列中的任務。這裡,移動任務至執行佇列的檢測者謂之“生產者”,任務執行執行緒謂之“消費者”。
3.1基本結構
執行器維護一個超時任務清單和一個執行佇列,一個超時檢測者,它使用定時器檢測任務是否超時並將超時的加入執行佇列,一個任務執行者負責執行超時任務中的回撥過程。
3.2資料結構
超時任務資訊,除了呼叫者傳遞的物件標識、超時時長與回撥方法,還包括其它執行過程中所需的屬性:任務標識、執行時間點。詳細資訊看如下類定義:
/// <summary>
/// 超時回撥的委託
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="objectKey"></param>
public delegate void TimeoutCallback<T>(T objectKey);
/// <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; }
}
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);
}
else
{
// 等待生產者通知
_WaitHandle.WaitOne();
}
}
}
3.7向外開放的介面
程式碼如是說:
/// <summary>
/// 指定物件標識,超時時長(秒為單位),超時執行回撥,加入到超時檢測字典中
/// </summary>
/// <param name="objectKey"></param>
/// <param name="timeoutSeconds"></param>
/// <param name="callback"></param>
/// <returns></returns>
public long AddTimeoutTask(T objectKey, int timeoutSeconds, TimeoutCallback<T> callback)
{
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;
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) =>
{
Console.WriteLine(key + " is timeout.");
};
runner.AddTimeoutTask("a", 4, callback);
runner.AddTimeoutTask("b", 3, callback);
runner.AddTimeoutTask("c", 2, callback);
Console.ReadKey();
runner.Dispose();
}
}
執行結果:
5.小結
超時處理在非同步通訊中經常會碰到,實現超時處理的通用機制,能有效的複用程式碼,提高效率。程式碼仍然有很多優化空間,如遍歷檢測超時是否有更合適的的方式等,歡迎探討!
完整程式碼請訪問: