1?? 背景與定位
在 .NET Framework 2.0 時代,微軟引入了 BackgroundWorker
來解決 WinForm/WPF 場景下“耗時操作阻塞 UI 線程”的問題;而 Component
早在 1.0 就已存在,是所有可視化/非可視化設計器的“基類”。理解這兩者的源碼與機制,可以幫助我們:
更精確地調試遺留代碼;
在需要設計時支持時(如自定義控件/組件)少走彎路;
在無法使用 async-await 的老項目中做最小侵入式改造。
2?? Component 類:所有“組件”的鼻祖?
2.1 命名空間與程序集
// 所在程序集:System.ComponentModel.Primitives.dll
namespace System.ComponentModel
2.2 繼承鏈與接口實現
System.Object└─ System.MarshalByRefObject // 支持跨 AppDomain 遠程調用└─ System.ComponentModel.Component├─ 實現 IComponent // 提供 Site 與 Disposed 事件└─ 實現 IDisposable // 標準 Dispose 模式
2.3 設計時元數據
[ComVisible(true)]
:可被 COM 調用。[ClassInterface(ClassInterfaceType.AutoDispatch)]
:為 COM 客戶端生成雙重接口。[DesignerCategory("Component")]
:告訴 Visual Studio 使用 ComponentDesigner。
2.4 核心字段與線程安全?
private static readonly object EventDisposed = new object();
private ISite site;
private EventHandlerList events; // 延遲創建,減少內存
所有事件都通過
EventHandlerList
存儲,避免每個事件一個字段,節省內存。對
events
的訪問采用“延遲初始化 + 雙檢鎖”。
2.5 事件系統:EventHandlerList 的魔法?
public event EventHandler Disposed
{add { Events.AddHandler(EventDisposed, value); }remove { Events.RemoveHandler(EventDisposed, value); }
}
通過
object
類型的 key(而非字符串)避免沖突。EventHandlerList
內部使用單向鏈表,添加/刪除 O(1)。
2.6 Dispose 模式深度解析?
~Component() { Dispose(false); }public void Dispose()
{Dispose(true);GC.SuppressFinalize(this);
}protected virtual void Dispose(bool disposing)
{if (!disposing) return;lock (this){if (site?.Container != null)site.Container.Remove(this); // 設計器撤銷時自動清理((EventHandler)events?[EventDisposed])?.Invoke(this, EventArgs.Empty);}
}
標準 Dispose 模式 + lock(this) 保證線程安全。
在 VS 設計器中,當用戶從設計面板上刪除組件時,
Container.Remove
會觸發Dispose
。
2.7 設計時支持:Site / Container / DesignMode?
ISite
:為組件提供“宿主”信息(名稱、容器、設計模式標志、全局服務)。IContainer
:管理一組組件的生命周期。DesignMode
:運行時永遠返回 false,僅在設計器進程返回 true。
2.8 實戰:自定義一個可設計時拖拽的組件?
[ToolboxItem(true)]
public class MyLogger : Component
{public string LogPath { get; set; }public void Write(string text){File.AppendAllText(LogPath, text + Environment.NewLine);}protected override void Dispose(bool disposing){// 清理文件句柄等資源base.Dispose(disposing);}
}
編譯后將 dll 添加到工具箱即可拖拽到 WinForm 設計器。
可在屬性窗口編輯
LogPath
。
3?? BackgroundWorker 類:WinForm/WPF 時代的“輕量 Task”?
3.1 背景與演進史
.NET 2.0 引入,解決 WinForm 中“跨線程訪問 UI”痛點。
.NET 4.0 之后官方推薦使用 Task + Progress<T>
但在老項目、設計器生成代碼、腳本環境中仍大量存在。
3.2 類型聲明與特性?
[SRDescription("BackgroundWorker_Desc")] // 本地化資源
[DefaultEvent("DoWork")] // 雙擊控件默認生成 DoWork 事件
[HostProtection(SecurityAction.LinkDemand, SharedState = true)]
HostProtection
告訴 CLR:若宿主禁止共享狀態(如 SQL Server CLR),則拒絕加載。
3.3 三大靜態鍵對象?
private static readonly object doWorkKey = new object();
private static readonly object runWorkerCompletedKey = new object();
private static readonly object progressChangedKey = new object();
用于
Events.AddHandler/remove
,保證事件 key 的唯一性。
3.4 狀態字段:isRunning、cancellationPending 的并發語義?
均為
bool
,讀寫均在 UI 線程 或 通過AsyncOperation.Post
封送,因此無需volatile
或Interlocked
。注意:在
DoWork
事件處理器中訪問CancellationPending
是線程安全的,因為CancelAsync
僅設置標志位,無額外鎖。
3.5 AsyncOperation & SynchronizationContext?
AsyncOperationManager.CreateOperation(null)
捕獲當前SynchronizationContext
(WinForm/WPF Dispatcher)。通過
Post
/PostOperationCompleted
把回調封送回 UI 線程,避免跨線程更新控件。
3.6 三大事件?
事件名 | 觸發線程 | 典型用途 | |
DoWork | 后臺線程 |
| |
ProgressChanged | UI 線程(SynchronizationContext) | 更新進度條、日志 | |
RunWorkerCompleted | UI 線程 | 處理結果或異常、隱藏等待動畫 |
3.7 四大公開方法
RunWorkerAsync(object argument)
檢查
isRunning
,拋InvalidOperationException
防并發。創建
AsyncOperation
,然后threadStart.BeginInvoke(argument, null, null)
進入線程池。
CancelAsync()?
檢查
WorkerSupportsCancellation
,設置cancellationPending = true
。
ReportProgress(int percent, object userState)?
檢查
WorkerReportsProgress
,通過asyncOperation.Post(progressReporter, args)
封送。
Dispose(bool)
(繼承自 Component)?
在 WinForm 設計器中移除控件時會調用,確保線程清理。
3.8 兩大回調委托?
private readonly WorkerThreadStartDelegate threadStart; // 線程入口
private readonly SendOrPostCallback operationCompleted; // 完成回調
private readonly SendOrPostCallback progressReporter; // 進度回調
WorkerThreadStartDelegate
本質上是delegate void WorkerThreadStartDelegate(object argument)
,避免每次構造新委托。
3.9 WorkerThreadStart 流程圖?
?
?3.10 線程模型與死鎖陷阱
若
DoWork
中 阻塞 UI 線程(如Invoke
等待 UI 回復),將發生死鎖。正確做法:僅通過
ReportProgress
與 UI 交互。
?3.11 性能考量:與 Task 對比
維度 | BackgroundWorker | Task + Progress<T> | |
語言級 async-await | ? | ? | |
線程池調度 | ? | ? | |
進度報告 | 事件 | IProgress<T> | |
取消 |
| CancellationToken | |
內存分配 |
| 結構體 + lambda |
結論:新項目優先 Task;維護老代碼可保留 BGW。
3.12 跨平臺注意點
?
.NET Core 3.0+ 仍支持 BGW,但設計器(WinForm Designer)僅在 Windows 上可用。
ASP.NET Core 無 SynchronizationContext,默認會把回調丟到 ThreadPool,UI 更新需手動封送。
3.13 實戰:文件復制器(含 WinForm UI)?
設計界面:ProgressBar、TextBox(文件名)、Button(開始/取消)。
代碼:
var bgw = new BackgroundWorker {WorkerReportsProgress = true,WorkerSupportsCancellation = true };bgw.DoWork += (_, e) => {var src = (string)e.Argument;var dst = Path.ChangeExtension(src, ".bak");using var fin = File.OpenRead(src);using var fout = File.Create(dst);var buffer = new byte[4096];long total = fin.Length, read = 0;int count;while ((count = fin.Read(buffer, 0, buffer.Length)) > 0){if (bgw.CancellationPending) { e.Cancel = true; return; }fout.Write(buffer, 0, count);read += count;bgw.ReportProgress((int)(read * 100 / total));}e.Result = dst; };bgw.ProgressChanged += (_, e) => progressBar1.Value = e.ProgressPercentage; bgw.RunWorkerCompleted += (_, e) => {if (e.Cancelled) MessageBox.Show("已取消");else if (e.Error != null) MessageBox.Show(e.Error.Message);else MessageBox.Show("完成,輸出:" + e.Result); }; bgw.RunWorkerAsync(textBox1.Text);
3.14 在 ASP.NET Core 中的“逆向”用法
?雖然不推薦,但可在后臺服務中模擬:
services.AddHostedService(sp => new BgwHostedService());
由于沒有 UI SynchronizationContext,所有
ProgressChanged
會在 ThreadPool 線程觸發,需自行加鎖。
4?? 可維護性最佳實踐?
永遠檢查
IsBusy
,避免多次RunWorkerAsync
。在
DoWork
中 絕不 直接訪問 UI 元素。若業務復雜,考慮用 Task + Progress + CancellationTokenSource 重構。
設計時組件務必重寫
Dispose(bool)
釋放非托管資源。使用
SRDescription
/SRCategory
為你的組件提供本地化與分類支持。
?