一種極簡的異步超時處理機制設(shè)計與實現(xiàn)(C#版)

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)化空間,如遍歷檢測超時是否有更合適的的方式等感混,歡迎探討端幼!

完整代碼請訪問:

https://github.com/triplestudio/TimeoutTask

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市弧满,隨后出現(xiàn)的幾起案子婆跑,更是在濱河造成了極大的恐慌,老刑警劉巖谱秽,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件洽蛀,死亡現(xiàn)場離奇詭異,居然都是意外死亡疟赊,警方通過查閱死者的電腦和手機郊供,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來近哟,“玉大人驮审,你說我怎么就攤上這事〖矗” “怎么了疯淫?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長戳玫。 經(jīng)常有香客問我熙掺,道長,這世上最難降的妖魔是什么咕宿? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任币绩,我火速辦了婚禮蜡秽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘缆镣。我一直安慰自己芽突,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布董瞻。 她就那樣靜靜地躺著寞蚌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪钠糊。 梳的紋絲不亂的頭發(fā)上挟秤,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天,我揣著相機與錄音眠蚂,去河邊找鬼煞聪。 笑死,一個胖子當(dāng)著我的面吹牛逝慧,可吹牛的內(nèi)容都是我干的昔脯。 我是一名探鬼主播,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼笛臣,長吁一口氣:“原來是場噩夢啊……” “哼云稚!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起沈堡,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤静陈,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后诞丽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鲸拥,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年僧免,在試婚紗的時候發(fā)現(xiàn)自己被綠了刑赶。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡懂衩,死狀恐怖撞叨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情浊洞,我是刑警寧澤牵敷,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站法希,受9級特大地震影響枷餐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜苫亦,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一毛肋、第九天 我趴在偏房一處隱蔽的房頂上張望奕锌。 院中可真熱鬧,春花似錦村生、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至肄鸽,卻和暖如春卫病,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背典徘。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工蟀苛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人逮诲。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓帜平,卻偏偏與公主長得像,于是被迫代替她去往敵國和親梅鹦。 傳聞我的和親對象是個殘疾皇子裆甩,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,612評論 2 350