在開發 Windows Forms 應用程式時,經常會遇到背景執行緒需要更新 UI 控制項的情況。由於 Windows Forms 控制項並非執行緒安全,直接從非 UI 執行緒存取控制項會導致不可預期的行為或例外。本文將介紹一個自訂的單例 Timer 元件,並示範如何正確地使用 ISynchronizeInvoke 介面,將事件從背景執行緒封送至 UI 執行緒。
作者建立了一個名為 MyTimer.Timer 的單例計時器,它根據資料記錄中的 ID 與設定分鐘數觸發事件。然而,當事件處理常式位於一個 Form 類別中時,直接呼叫事件會導致跨執行緒存取問題。原始碼中第 82–93 行被註解的程式碼正是解決此問題的關鍵:它檢查事件目標是否實作 ISynchronizeInvoke,並透過 BeginInvoke 將呼叫封送到正確的執行緒。
internal class TimerNode
{
public long TimeCount { get; set; }
public object EvtID { get; }
public TimerNode(long timeCount, object evtID)
{
TimeCount = timeCount;
EvtID = evtID;
}
}
public class TimerEventArgs : EventArgs
{
public ArrayList EvtIDs { get; }
public TimerEventArgs(ArrayList evtIDs)
{
EvtIDs = evtIDs;
}
}
public delegate void TimerEventHandler(TimerEventArgs e);
public class Timer
{
public static event TimerEventHandler TimeCome;
private static readonly long Period = 1000 * 60; // 1 分鐘
private static readonly System.Threading.Timer _timer;
private static readonly Hashtable _timerNodes = new Hashtable();
static Timer()
{
_timer = new System.Threading.Timer(TimeArrive, null, Timeout.Infinite, Period);
}
public static void Run() => _timer.Change(0, Period);
public static void Stop() => _timer.Change(Timeout.Infinite, Period);
public static void Add(object evtID, long timeCount)
{
if (_timerNodes.ContainsKey(evtID))
((TimerNode)_timerNodes[evtID]).TimeCount = timeCount;
else
_timerNodes.Add(evtID, new TimerNode(timeCount, evtID));
}
public static void Remove(object evtID)
{
if (_timerNodes.ContainsKey(evtID))
_timerNodes.Remove(evtID);
}
private static void TimeArrive(object state)
{
var triggeredIDs = new ArrayList();
foreach (TimerNode node in _timerNodes.Values)
{
node.TimeCount--;
if (node.TimeCount <= 0)
triggeredIDs.Add(node.EvtID);
}
if (triggeredIDs.Count > 0)
{
foreach (object id in triggeredIDs)
_timerNodes.Remove(id);
RaiseTimeCome(new TimerEventArgs(triggeredIDs));
}
}
private static void RaiseTimeCome(TimerEventArgs e)
{
if (TimeCome == null) return;
// 關鍵:跨執行緒封送
if (TimeCome.Target is ISynchronizeInvoke syncInvoke)
{
if (syncInvoke.InvokeRequired)
{
syncInvoke.BeginInvoke(TimeCome, new object[] { e });
}
else
{
TimeCome(e);
}
}
else
{
TimeCome(e);
}
}
}
Form、Control)實作此介面,提供 InvokeRequired、BeginInvoke、Invoke 方法,用於跨執行緒封送。BeginInvoke 為非同步,不會阻塞背景執行緒;Invoke 為同步,會等待 UI 執行緒處理完成。此處使用 BeginInvoke 避免死結風險。TimeCome 可能有多個訂閱者,Target 僅代表第一個訂閱者的目標物件。更穩健的做法是遍歷 TimeCome.GetInvocationList(),逐個檢查並封送每個委派。若事件有多個訂閱者且分屬不同執行緒,建議修改 RaiseTimeCome 如下:
private static void RaiseTimeCome(TimerEventArgs e)
{
if (TimeCome == null) return;
foreach (TimerEventHandler handler in TimeCome.GetInvocationList())
{
if (handler.Target is ISynchronizeInvoke syncInvoke && syncInvoke.InvokeRequired)
{
syncInvoke.BeginInvoke(handler, new object[] { e });
}
else
{
handler(e);
}
}
}
正確處理跨執行緒事件封送是 WinForms 多線程開發的基礎功夫。透過 ISynchronizeInvoke 機制,確保 UI 更新始終在 UI 執行緒執行,能有效避免競爭條件與非預期錯誤。本文提供的單例計時器範例可作為專案中定時任務調度的起點。