正在從事以休閒為目的Coding活動時,忽然有個衝動想在C#中也用一下Javascript裡常用的setTimeout/clearTimeout。

setTimeout說穿了就是透過另一條Thread執行程式產生非同步效果,用.NET實作是小菜一碟,而我想挑戰的是如何用最簡潔的方法實作出來。

剛好這陣子陸續玩過Action<T> and Func<T>Closure in C#,加上研究Parallel.For()時被迫反覆寫了十來次,現在已經練就信手就可掰出一段Thread配Lambda範例的境界。這個題目拿來作為隨堂考試再適合也不過了。

在Action加Lambda加Closure的加持之下,只要不到30行程式就可以在C#中使用SetTimeout/ClearTimeout囉~~~ C#真是簡潔有力的好語言呀!

先看程式碼,後面再提一下重點:

using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Threading;
 
namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
 
        private void Form1_Load(object sender, EventArgs e)
        {
            //No UI thread issue, use SetTimeout(cb, delay)
            SetTimeout(() =>
            {
                MessageBox.Show("First");
            }, 2000);
            SetTimeout(() =>
            {
                listBox1.Items.Add("Second");
            }, 4000, this);
            Guid hnd = SetTimeout(() =>
            {
                listBox1.Items.Add("Third");
            }, 6000, this);
            SetTimeout(() =>
            {
                listBox1.Items.Add("Forth");
            }, 8000, this);
            ClearTimeout(hnd);
        }
 
        #region SetTimeout/ClearTimeout Simulation
        //Dictionary for running setTimeout
        static Dictionary<Guid, Thread> _setTimeoutHandles =
            new Dictionary<Guid, Thread>();
        //SetTimeout for no UI Thread issue
        static Guid SetTimeout(Action cb, int delay)
        {
            return SetTimeout(cb, delay, null);
        }
        //Javascript-style SetTimeout function
        //remember to set uiForm argument when there cb is trying
        //to change UI controls in window form
        //it will return a GUID as handle for cancelling
        static Guid SetTimeout(Action cb, int delay, Form uiForm)
        {
            Guid g = Guid.NewGuid();
            Thread t = new Thread(() =>
            {
                Thread.Sleep(delay);
                _setTimeoutHandles.Remove(g);
                if (uiForm != null)
                    //use Invoke() to avoid threading issue
                    //Ref: http://tinyurl.com/yjckzhz
                    uiForm.Invoke(cb);
                else
                    cb();
            });
            _setTimeoutHandles.Add(g, t);
            t.Start();
            return g;
        }
        //Javascript-style ClearTimeout
        static void ClearTimeout(Guid g)
        {
            if (!_setTimeoutHandles.ContainsKey(g))
                return;
            _setTimeoutHandles[g].Abort();
            _setTimeoutHandles.Remove(g);
        }
        #endregion
    }
}

重點補充:

  1. SetTimeout的原理是呼叫端傳入Action參數及延遲微秒(ms)數後,立即新增一條Thread執行"先Thread.Delay()指定時間長度再呼叫所傳入的Action",並將控制權交回呼叫端,邏輯十分單純。
  2. 由於SetTimeout傳入要延遲執行的Action程式實際上會由另一條Thread執行,若在該邏輯中變動Window Form上的元素,就會違背不可透過非UI Thread去更動UI元素的規則。因此,我們再多增加一個Form參數,當有需要時,透過Form.Invoke()間接執行才可避開UI Thread限制。
  3. 要能ClearTimeout,就必須保留Thread變數,必要時將其Abort()掉。
    我用了一個Diction<Guid, Thread>來保存Thread,SetTimeout時Dictionary.Add()並傳回一個GUID,ClearTimeout時可憑該GUID去中止尚未執行的排程。
  4. 理論上,ClearTimeout只能中止"仍在等待執行的工作",因此Thread.Delay一結束時,就立刻Dictionary.Remove(GUID) (這裡剛好展現了Closure的美妙之處),不再允許ClearTimeout,否則程式都跑了一半還胡亂Thread.Abort()會出人命的。

寫完再回頭看,短短30行Code還真扯到不少進階的東西(Action, Closure, Threading),看懂的人C# Coding能力應該都在中階以上了吧!(純個人意見,勿戰) 對這一段還不熟的人可以參考以前PO過的幾篇文章當起頭,但我想還需要參考官方文件、網路範例配合實地動手寫過才容易完全理解。

【延伸閱讀】

  1. Lambda演算式
  2. 如何透過Lambda精簡Threading程式
  3. Action<T> and Func<T>
  4. UI Thread限制
  5. Closure in C#

Comments

# by chicken

提供一個簡單一點的作法... .NET FX 裡光是 timer 就有三種之多 =_=,其中掛在 System.Threading 這個 namespace 裡的 Timer, 作用就是要解決這篇的問題... timer 時間到了,會直接把工作丟到 thread pool 裡去執行,用這個 timer 正好可以省掉自己控制 thread 的細節..

# by Billy

似乎用System.Timers.Timer 是最好的方法了。 讓我補充兩種.net timer 的reference: System.Timers.Timer http://msdn.microsoft.com/en-us/library/system.timers.timer.aspx System.Threading.Timer http://msdn.microsoft.com/en-us/library/system.threading.timer.aspx Their comparison: http://msdn.microsoft.com/en-us/magazine/cc164015.aspx#S2

# by Jeffrey

<p>to chicken, 一開始有考慮過System.Threading.Timer, 不過卡在一個迷思(心魔? 呵),即便把period設成Timeout.Infinite,但Timer生命週期仍會持續,必須要手動銷毁物件才真的會被回收,若要加上額外銷毁機制就未必划算。(但我不確定這個推想是否為真,得實際玩看看才知) </p> <p>另外,ClearTimeout時是否能避開執行中的Callback也待研究(但背後若是用ThreadPool的話應該不會有問題),不像自己控管Thread這麼一目了然。</p> <p>to Billy, System.Timers.Timer可以自動解決UI Thread限制是一大賣點,不過不像Threading.Timer有dueTime可以簡單做出延遲執行效果。而生命週期及ClearTimeout的問題也跟Threading.Timer同。</p> <p>看來幾個Solution各有優劣,以乎沒有一面倒的結論。</p>

Post a comment