首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 多線程:跨線程透過事件委托封送調用方法

     編輯:關於C語言

前言

在開發 Windows Forms 應用程式時,經常會遇到背景執行緒需要更新 UI 控制項的情況。由於 Windows Forms 控制項並非執行緒安全,直接從非 UI 執行緒存取控制項會導致不可預期的行為或例外。本文將介紹一個自訂的單例 Timer 元件,並示範如何正確地使用 ISynchronizeInvoke 介面,將事件從背景執行緒封送至 UI 執行緒。

問題背景

作者建立了一個名為 MyTimer.Timer 的單例計時器,它根據資料記錄中的 ID 與設定分鐘數觸發事件。然而,當事件處理常式位於一個 Form 類別中時,直接呼叫事件會導致跨執行緒存取問題。原始碼中第 82–93 行被註解的程式碼正是解決此問題的關鍵:它檢查事件目標是否實作 ISynchronizeInvoke,並透過 BeginInvoke 將呼叫封送到正確的執行緒。

核心程式碼解析

1. 定時器節點與事件參數

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);

2. 單例 Timer 類別

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);
        }
    }
}

關鍵點說明

  • ISynchronizeInvoke:Windows Forms 控制項(如 Form、Control)實作此介面,提供 InvokeRequired、BeginInvoke、Invoke 方法,用於跨執行緒封送。
  • BeginInvoke vs 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 執行緒執行,能有效避免競爭條件與非預期錯誤。本文提供的單例計時器範例可作為專案中定時任務調度的起點。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved