C# 线程问题
固定加锁顺序(最有效、最常用)
所有线程按相同顺序获取多个锁。
坏例子(会死锁):
// 线程1
lock (objA) {
lock (objB) { }
}
// 线程2
lock (objB) {
lock (objA) { }
}
好例子(统一顺序):
// 永远先锁 A,再锁 B
lock (objA)
lock (objB)
{
// 业务
}
2. 减少锁嵌套,尽量不要同时拿多个锁
能一个锁解决,就不要用两个。能拆成小方法,就不要一大段嵌套锁。
3. 使用 TryLock 超时,不无限等待
C# 的 Monitor.TryEnter 可以设置超时,避免死等。
if (Monitor.TryEnter(lockObj, 100))
{
try {
// 逻辑
}
finally {
Monitor.Exit(lockObj);
}
}
else
{
// 获取失败,重试或放弃
}
lock 关键字做不到超时,必须用 Monitor。
4. 使用更高级的同步工具(替代 raw lock)
SemaphoreSlim(可设置上限、可异步)
ReaderWriterLockSlim(读写分离)
AsyncLock(异步锁,避免 async/await 死锁)
异步代码千万不要用 lock,会死锁!
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1);
await _semaphore.WaitAsync();
try
{
// 异步逻辑
}
finally
{
_semaphore.Release();
}
5. 避免在锁内调用未知代码 / 回调
锁里不要调:
外部事件
委托
虚方法
第三方代码
别人代码可能又去锁别的东西,形成隐形循环等待。
6. 锁粒度尽可能小
锁只包必须保护的代码,不要一大段全锁住。锁越短,死锁概率越低。
7. 使用线程安全集合,避免手动 lock
C# 自带线程安全集合:
ConcurrentQueue
ConcurrentDictionary<TKey,TValue>
BlockingCollection
它们内部已经处理好并发,你不用写 lock。
三、最简单记忆口诀
锁顺序统一
不嵌套多个锁
不用 lock 套异步
TryLock 加超时
优先用并发集合**
一、锁的使用原则(最核心)
- 锁对象必须是 private readonly
不要用 this、不要用 string、不要用公开对象。
private readonly object _lockObj = new object();
2. 永远不要锁 this / 字符串 / 基础类型
锁 this 外部可能也在锁,造成隐形死锁
字符串被 CLR 共享,会全局互斥
值类型会被装箱,每次锁的不是同一个对象
3. 临界区越短越好
只锁共享变量读写,不要把 IO、Sleep、数据库、网络包进锁。
4. 不要在锁里调用外部代码 / 事件 / 委托
极易形成隐式嵌套锁 → 死锁重灾区。
5. 多个锁必须固定全局加锁顺序
例如永远:先锁 A → 再锁 B → 再锁 C彻底杜绝循环等待。
二、async/await 下的线程安全(上位机高频)
- async 方法绝对不要用 lock
lock 不支持异步切换,会直接导致线程错乱 / 死锁。 - 异步锁用 SemaphoreSlim(1)
private readonly SemaphoreSlim _asyncLock = new(1);
async Task SafeAsync()
{
await _asyncLock.WaitAsync();
try
{
// 异步逻辑:PLC、HTTP、数据库
}
finally
{
_asyncLock.Release();
}
}
- 不要用 Task.Run 包裹同步锁来假装异步
只会制造隐藏死锁。
三、共享变量安全规则
- 尽量减少共享变量
能局部变量就不全局,能单线程就不多线程。 - 共享状态只允许一个入口修改
比如统一用一个 WriteData() 方法修改,外部禁止直接改字段。 - 只读 /const/ 不可变对象天然线程安全
能设计成不可变就不要设计可变。 - 简单类型读写也要小心
long/double 不是原子操作,必须加锁或用 Interlocked。
四、优先使用 .NET 内置线程安全工具
- 线程安全集合(上位机采集必用)
ConcurrentQueue<T>
ConcurrentDictionary<TKey,T>
BlockingCollection<T>
不用自己写 lock,性能还更好。
- 原子操作用 Interlocked
适合计数、开关、标记:
Interlocked.Increment(ref _count);
Interlocked.Exchange(ref _isRunning, 0);
无锁、极快。
- 读写场景用 ReaderWriterLockSlim
读多写少非常适合:
配置读取
实时数据展示
日志、状态查询
五、UI 线程安全(WPF/WinForms)
- 只能在 UI 线程更新控件
跨线程直接赋值会抛异常。 - WPF 使用 Dispatcher
Application.Current.Dispatcher.Invoke(() =>{
lblStatus.Text = "连接成功";
});
- WinForms 使用 Invoke
if (this.InvokeRequired)
{
this.Invoke(() => label1.Text = "ok");
}
- 大量更新用 Dispatcher.BeginInvoke
避免阻塞工作线程。
六、死锁终极防御
- 能用单线程 + 队列就不用多线程互斥
工业上位机非常实用模式:
采集线程只入队
处理线程单线程消费
完全无锁、无死锁。 - 使用 Monitor.TryEnter 带超时
绝不无限等待:
if (Monitor.TryEnter(_lock, 300))
{
try { ... }
finally { Monitor.Exit(_lock); }
}
else
{
// 失败日志、重试、降级
}
- 避免锁嵌套
能拆成独立方法就不要嵌套多层锁。
七、上位机 / 工控特别实用模式
- 生产者消费者模型(无锁神器)
private BlockingCollection<DataPack> _queue = new();
// 采集线程
_queue.Add(data);
// 处理线程(单线程消费)
foreach(var item in _queue.GetConsumingEnumerable())
{
// 处理
}
最稳、最简单、永不死锁。
2. 单例服务 + 统一锁
PLC、串口、运动控制只开一个实例,统一加锁访问。
3. 状态机 + 原子标记
用 Interlocked 控制启动 / 停止,避免重入。
八、最精简口诀(背会够用)
锁要私有只读,不锁 this 和字符串
异步不用 lock,用 SemaphoreSlim
临界区越短越好,不套 Sleep/IO/ 回调
多锁固定顺序,杜绝循环等待
能用 Concurrent 集合就不用手锁
能单线程消费队列,就不多线程抢锁
UI 必须走 Dispatcher/Invoke
工业场景优先 TryLock 超时
更多推荐

所有评论(0)