固定加锁顺序(最有效、最常用)

所有线程按相同顺序获取多个锁。
坏例子(会死锁):

// 线程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 加超时
优先用并发集合**

一、锁的使用原则(最核心)

  1. 锁对象必须是 private readonly
    不要用 this、不要用 string、不要用公开对象。
private readonly object _lockObj = new object();

2. 永远不要锁 this / 字符串 / 基础类型

锁 this 外部可能也在锁,造成隐形死锁
字符串被 CLR 共享,会全局互斥
值类型会被装箱,每次锁的不是同一个对象

3. 临界区越短越好

只锁共享变量读写,不要把 IO、Sleep、数据库、网络包进锁。

4. 不要在锁里调用外部代码 / 事件 / 委托

极易形成隐式嵌套锁 → 死锁重灾区。

5. 多个锁必须固定全局加锁顺序

例如永远:先锁 A → 再锁 B → 再锁 C彻底杜绝循环等待。

二、async/await 下的线程安全(上位机高频)

  1. async 方法绝对不要用 lock
    lock 不支持异步切换,会直接导致线程错乱 / 死锁。
  2. 异步锁用 SemaphoreSlim(1)
private readonly SemaphoreSlim _asyncLock = new(1);

async Task SafeAsync()
{
    await _asyncLock.WaitAsync();
    try
    {
        // 异步逻辑:PLC、HTTP、数据库
    }
    finally
    {
        _asyncLock.Release();
    }
}
  1. 不要用 Task.Run 包裹同步锁来假装异步
    只会制造隐藏死锁。

三、共享变量安全规则

  1. 尽量减少共享变量
    能局部变量就不全局,能单线程就不多线程。
  2. 共享状态只允许一个入口修改
    比如统一用一个 WriteData() 方法修改,外部禁止直接改字段。
  3. 只读 /const/ 不可变对象天然线程安全
    能设计成不可变就不要设计可变。
  4. 简单类型读写也要小心
    long/double 不是原子操作,必须加锁或用 Interlocked。

四、优先使用 .NET 内置线程安全工具

  1. 线程安全集合(上位机采集必用)
ConcurrentQueue<T>
ConcurrentDictionary<TKey,T>
BlockingCollection<T>

不用自己写 lock,性能还更好。

  1. 原子操作用 Interlocked
    适合计数、开关、标记:
Interlocked.Increment(ref _count);
Interlocked.Exchange(ref _isRunning, 0);

无锁、极快。

  1. 读写场景用 ReaderWriterLockSlim
    读多写少非常适合:
    配置读取
    实时数据展示
    日志、状态查询

五、UI 线程安全(WPF/WinForms)

  1. 只能在 UI 线程更新控件
    跨线程直接赋值会抛异常。
  2. WPF 使用 Dispatcher
Application.Current.Dispatcher.Invoke(() =>{
    lblStatus.Text = "连接成功";
});
  1. WinForms 使用 Invoke
if (this.InvokeRequired)
{
    this.Invoke(() => label1.Text = "ok");
}
  1. 大量更新用 Dispatcher.BeginInvoke
    避免阻塞工作线程。

六、死锁终极防御

  1. 能用单线程 + 队列就不用多线程互斥
    工业上位机非常实用模式:
    采集线程只入队
    处理线程单线程消费
    完全无锁、无死锁。
  2. 使用 Monitor.TryEnter 带超时
    绝不无限等待:
if (Monitor.TryEnter(_lock, 300))
{
    try { ... }
    finally { Monitor.Exit(_lock); }
}
else
{
    // 失败日志、重试、降级
}
  1. 避免锁嵌套
    能拆成独立方法就不要嵌套多层锁。

七、上位机 / 工控特别实用模式

  1. 生产者消费者模型(无锁神器)
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 超时

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐