事件委托非线程安全,多线程调用Invoke可能崩溃;应使用Interlocked.CompareExchange获取委托快照再调用;事件触发不负责线程切换,订阅者需自行处理跨线程UI更新等线程契约问题。

事件委托本身不是线程安全的
直接在多线程环境中调用 eventHandler?.Invoke(...) 有崩溃风险:如果某线程正在订阅/取消订阅(即修改委托链),另一线程同时调用 Invoke,可能抛出 NullReferenceException 或 InvalidOperationException。这不是“偶尔出错”,而是在高并发下必然发生的问题。
用 Interlocked.CompareExchange 复制委托快照
核心思路是:在触发前,原子地读取当前委托引用,避免后续修改影响本次调用。这是 .NET 官方推荐做法,比加锁更轻量且无死锁风险。
public event EventHandlerDataReceived; protected virtual void OnDataReceived(DataEventArgs e) { // 原子读取当前委托引用,生成快照 var handler = Interlocked.CompareExchange(ref DataReceived, null, null); handler?.Invoke(this, e); }
注意:Interlocked.CompareExchange(ref field, null, null) 不改变字段值,只返回其当前值——这正是我们需要的“安全快照”。
避免在事件处理中长时间阻塞或引发新线程
即使触发逻辑安全,事件订阅者的实现仍可能破坏整体线程模型:
- 不要在 UI 线程事件(如 WinForms 的
Button.Click)里直接触发耗时操作,否则界面冻结 - 不要在事件处理器里直接开新线程(如
Task.Run)去调用另一个事件——容易形成嵌套竞争 - 若需异步响应,明确区分“通知”和“响应”:触发事件后,由监听方自行决定同步处理、调度到线程池,或丢给
Task处理
需要跨线程更新 UI 时,必须走同步上下文
比如从后台线程触发事件,而某个订阅者要更新 WPF 的 TextBox.Text,直接调用会抛 InvalidOperationException: “The calling thread cannot access this object because a different thread owns it.”。
此时不能靠事件机制自动解决,必须由订阅者自己适配:
// WPF 订阅示例
dataProcessor.DataReceived += (s, e) =>
{
if (Application.Current.Dispatcher.CheckAccess())
{
statusText.Text = e.Message;
}
else
{
Application.Current.Dispatcher.Invoke(() => statusText.Text = e.Message);
}
};
WinForms 同理用 Control.InvokeRequired + Invoke。事件本身不负责线程切换,那是消费者的责任。
真正麻烦的从来不是“怎么触发”,而是“谁在哪儿响应、响应时持有啥资源、是否共享状态”。安全触发只是第一道关卡,后面每层订阅逻辑都得单独审视线程契约。










