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
为你的组件提供本地化与分类支持。