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

C# 多執行緒進階:背景工作執行緒與 UI 執行緒分離、資料傳遞、安全更新 UI

     編輯:關於C語言

核心問題

WinForms/WPF 應用中,**UI 執行緒 (主執行緒) 單一且負責訊息泵**,若在 UI 執行緒執行耗時運算 (資料庫查詢、檔案 I/O、網路呼叫) 會導致介面凍結、無法回應使用者操作。正確架構:**UI 執行緒僅負責繪製與事件**,**耗時工作移至背景執行緒**,完成後**安全地回傳結果更新 UI**。

演進三階段

階段 1:直接在背景執行緒存取 UI 控制項 (錯誤示範)

// ❌ 錯誤:跨執行緒存取控制項屬性,會拋 InvalidOperationException (Debug 模式) 或導致未定義行為
private void btnStart_Click(object sender, EventArgs e)
{
    new Thread(() =>
    {
        for (int i = 0; i < 100; i++)
        {
            Thread.Sleep(50);
            listView1.Items[i].SubItems[1].Text = i.ToString(); // 直接存取 UI
        }
    }).Start();
}

階段 2:Control.Invoke / BeginInvoke 切換至 UI 執行緒 (傳統修正)

// ✅ 正確:使用 Invoke 將委派排入 UI 訊息佇列執行
private void btnStart_Click(object sender, EventArgs e)
{
    new Thread(Worker) { IsBackground = true }.Start();
}

private void Worker()
{
    for (int i = 0; i < 100; i++)
    {
        Thread.Sleep(50);
        string text = i.ToString();
        
        // 檢查是否需要跨執行緒
        if (listView1.InvokeRequired)
        {
            listView1.Invoke(new Action(() => 
            {
                if (listView1.Items.Count > i)
                    listView1.Items[i].SubItems[1].Text = text;
            }));
        }
        else
        {
            listView1.Items[i].SubItems[1].Text = text;
        }
    }
}

階段 3:Task + async/await + IProgress<T> (現代最佳實務)

// ✅ 推薦:非同步、進度回報、取消支援、例外處理、自動切換同步內容
private async void btnStart_Click(object sender, EventArgs e)
{
    btnStart.Enabled = false;
    var progress = new Progress(i => 
    {
        if (listView1.Items.Count > i)
            listView1.Items[i].SubItems[1].Text = i.ToString();
    });
    
    var cts = new CancellationTokenSource();
    btnCancel.Click += (s, e) => cts.Cancel();

    try
    {
        await Task.Run(() => DoWork(progress, cts.Token), cts.Token);
        MessageBox.Show("完成");
    }
    catch (OperationCanceledException)
    {
        MessageBox.Show("已取消");
    }
    catch (Exception ex)
    {
        MessageBox.Show($"錯誤: {ex.Message}");
    }
    finally
    {
        btnStart.Enabled = true;
    }
}

private void DoWork(IProgress progress, CancellationToken token)
{
    for (int i = 0; i < 100; i++)
    {
        token.ThrowIfCancellationRequested();
        Thread.Sleep(50); // 模擬耗時運算
        progress.Report(i); // 自動捕獲呼叫端同步內容 (UI 執行緒)
    }
}

關鍵類別對比

類別/模式適用場景優點缺點
Thread + Invoke舊專案、需精細控制執行緒生命週期底層控制、可設定 Priority/IsBackground手動管理同步、易出錯、無取消/進度標準化
BackgroundWorker.NET Framework 2.0~4.0 專案內建進度/完成/取消事件、自動 Invoke已過時、不支援 async/await、單一背景作業
Task.Run + async/await現代 .NET (4.5+ / Core / 5+)語法簡潔、組合性強、支援取消/進度/例外傳播需理解同步內容、ThreadPool 星檔風險
IProgress<T> + Progress<T>背景作業回報進度至 UI自動捕獲 SynchronizationContext、解耦回報邏輯僅單向回報、高頻回報需節流
CancellationToken可取消的長時間作業標準化取消協議、可傳遞多層需定期檢查 ThrowIfCancellationRequested

進階模式:生產者-消費者 / 管線處理

// 使用 BlockingCollection 實作執行緒安全佇列
private readonly BlockingCollection _queue = new();
private readonly CancellationTokenSource _cts = new();

public void StartProducer()
{
    Task.Run(() =>
    {
        foreach (var item in GetDataSource())
        {
            _queue.Add(item, _cts.Token); // 佇列滿時阻塞
        }
        _queue.CompleteAdding();
    }, _cts.Token);
}

public void StartConsumer(int workerCount)
{
    for (int i = 0; i < workerCount; i++)
    {
        Task.Run(ConsumerLoop, _cts.Token);
    }
}

private void ConsumerLoop()
{
    foreach (var item in _queue.GetConsumingEnumerable(_cts.Token))
    {
        ProcessItem(item); // 耗時運算
        // 進度回報...
    }
}

UI 更新節流 (避免高頻 Report 造成訊息泵擁塞)

public class ThrottledProgress : IProgress
{
    private readonly IProgress _inner;
    private readonly TimeSpan _interval;
    private DateTime _lastReport = DateTime.MinValue;
    private T _latestValue;
    private readonly object _lock = new();

    public ThrottledProgress(IProgress inner, TimeSpan interval)
    {
        _inner = inner; _interval = interval;
    }

    public void Report(T value)
    {
        lock (_lock)
        {
            _latestValue = value;
            var now = DateTime.Now;
            if (now - _lastReport >= _interval)
            {
                _lastReport = now;
                _inner.Report(value);
            }
        }
    }

    // 確保最後一次值一定送達
    public void Flush()
    {
        lock (_lock)
        {
            _inner.Report(_latestValue);
        }
    }
}

// 使用
var throttled = new ThrottledProgress(progress, TimeSpan.FromMilliseconds(200));
await Task.Run(() => DoWork(throttled, token));
throttled.Flush();

常見錯誤與除錯技巧

  • Cross-thread operation not valid:忘記 InvokeRequired 檢查、或在非 UI 執行緒建立控制項。解決:統一用 Progress<T> 或 Invoke。
  • 死結:背景執行緒 Invoke 等待 UI,UI 執行緒又 Wait() 背景 Task。解決:UI 端一律 await,勿用 .Result/.Wait()。
  • 記憶體洩漏:匿名方法捕獲 this、事件未取消訂閱、CancellationToken 未釋放。解決:WeakReference、IDisposable 模式、using 區塊。
  • ThreadPool 星檔:大量長時間 Task.Run 佔用執行緒池。解決:TaskCreationOptions.LongRunning 建立專用執行緒、或使用 SemaphoreSlim 限制並行度。

除錯工具

  • Visual Studio **執行緒視窗** (Debug → Windows → Threads):檢視所有執行緒呼叫堆疊、凍結/解凍。
  • **平行堆疊** (Parallel Stacks):圖形化顯示執行緒等待關係,快速找出死結。
  • **工作視窗** (Tasks):查看 Task 狀態、呼叫堆疊、取消狀態。
  • Debug.WriteLine($"ThreadId: {Thread.CurrentThread.ManagedThreadId}"):追蹤程式碼執行緒歸屬。

總結:現代 WinForms 多執行緒範本

private async void OnStartClick(object sender, EventArgs e)
{
    var progress = new Progress(UpdateUI);
    var cts = new CancellationTokenSource();
    btnCancel.Click += (s, e) => cts.Cancel();
    SetUIEnabled(false);

    try
    {
        await Task.Run(() => LongRunningWork(progress, cts.Token), cts.Token);
        ShowMessage("完成");
    }
    catch (OperationCanceledException) { ShowMessage("取消"); }
    catch (Exception ex) { ShowError(ex); }
    finally { SetUIEnabled(true); }
}

private void LongRunningWork(IProgress progress, CancellationToken token)
{
    for (int i = 0; i < 100; i++)
    {
        token.ThrowIfCancellationRequested();
        // 耗時運算...
        progress.Report(i);
    }
}

private void UpdateUI(int value) => /* 安全更新控制項 */;
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved