非线程安全代码在多线程下并发访问会因竞争条件导致数据错乱、异常或静默损坏;应通过lock保护临界区、改用Concurrent集合,或隔离到单线程上下文执行。

为什么直接调用非线程安全代码会出问题
非线程安全的代码(比如某些旧版 System.Drawing 类、自定义的共享 Dictionary 实例、或未加锁的静态缓存)在多线程下被并发访问时,可能引发数据错乱、NullReferenceException、InvalidOperationException,甚至静默损坏状态。这不是“偶尔报错”,而是竞争条件(race condition)——结果不可预测,且难以复现。
用 lock 保护临界区是最直接的方式
当你要调用的非线程安全逻辑是短时、确定、且可识别边界的(例如更新一个共享计数器、写入一个全局日志缓冲区),用 lock 是最可控的选择。关键是锁对象必须唯一、私有、不可被外部修改。
- 不要锁
this、typeof(MyClass)或字符串字面量——它们可能被其他代码共用,导致意外阻塞或死锁 - 推荐声明一个
private readonly object _syncRoot = new object();作为锁对象 - 确保所有访问该共享资源的路径都经过同一把锁,包括读和写
private readonly object _syncRoot = new object();
private int _sharedCounter;
public void IncrementCounter()
{
lock (_syncRoot)
{
_sharedCounter++; // 非线程安全操作,现在受保护
}
}
用 ConcurrentQueue / ConcurrentDictionary 替代手写同步逻辑
如果你原本想用锁来保护一个集合操作(如“先查再删”、“如果不存在则添加”),直接换成 Concurrent* 类型通常更安全、更高效。它们内部使用细粒度锁或无锁算法,避免了你手动同步的疏漏。
-
ConcurrentDictionary.TryAdd(key, value)比 “先ContainsKey再Add” 更可靠 -
ConcurrentQueue.TryDequeue(out T item)不会因队列为空抛异常,也无需额外lock - 注意:
ConcurrentDictionary的Count属性不是原子的,遍历时仍需考虑迭代期间的变更
把非线程安全代码移到单线程上下文中执行
当非线程安全逻辑较重、或依赖不可重入的外部资源(如 COM 组件、某些 Win32 GDI 句柄),强行加锁反而容易引发死锁或 UI 响应问题。这时更适合把它“隔离”到一个明确的单线程环境:
- UI 线程:WPF/WinForms 中用
Dispatcher.Invoke或Control.Invoke - 专用后台线程:启动一个长期运行的
Thread,用BlockingCollection接收任务,顺序执行 - TaskScheduler:创建单线程调度器(
new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler),投递Task运行
这种方式牺牲一点吞吐,但彻底规避了同步复杂度——尤其适合封装成服务类,对外提供异步接口,内部自动序列化调用。
真正难的不是选哪种方案,而是识别“哪里不安全”:有些类文档没写线程安全性,有些方法看似只读却偷偷改了内部缓存。遇到不确定的第三方库,宁可默认按非线程安全处理,加一层隔离,也别赌它“应该没问题”。










