c# 如何避免死锁 c# 死锁的产生条件和排查方法

来源:这里教程网 时间:2026-02-21 17:41:31 作者:

死锁的四个必要条件,缺一不可

死锁不是“偶尔卡住”,而是满足四个条件后必然发生的系统僵局:

互斥条件
持有并等待
不可剥夺
循环等待
。只要其中任意一个被打破,死锁就不可能发生。

互斥条件
:比如
lock
语句块、
Mutex
Monitor
等,资源一次只能被一个线程进入
持有并等待
:线程已拿到
lockA
,又去申请
lockB
,但
lockB
正被别人拿着
不可剥夺
:C# 中没有强制释放锁的机制(不像数据库可 kill session),
lock
只能靠线程自己退出作用域或抛异常后释放
循环等待
:线程1等线程2的
lockB
,线程2又等线程1的
lockA
—— 这是最常见、最易复现的死锁形态

Monitor.TryEnter
加超时,快速止损

这不是根治,但能防止线程无限挂起,让问题暴露得更早、更可控。相比裸

lock
,它把“等不到就卡死”变成“等不到就放弃+报错”。

超时时间别设太短(如
TimeSpan.FromMilliseconds(10)
),否则高负载下容易误判;也别设太长(如
TimeSpan.FromMinutes(5)
),失去意义
推荐从
TimeSpan.FromSeconds(2)
起步,在日志中记录失败次数和堆栈,用于定位热点锁
注意:
TryEnter
成功后必须配对调用
Monitor.Exit
,否则会泄漏锁(
lock
语法糖自动处理这点,
TryEnter
不自动)
private static readonly object _sharedLock = new object();
public static bool TryUpdateData()
{
    if (Monitor.TryEnter(_sharedLock, TimeSpan.FromSeconds(2)))
    {
        try
        {
            // 执行临界区操作
            return true;
        }
        finally
        {
            Monitor.Exit(_sharedLock); // 必须显式释放!
        }
    }
    else
    {
        // 记录日志:在 2 秒内无法获取 _sharedLock,可能竞争激烈或已死锁
        Log.Warn("Failed to acquire lock within timeout");
        return false;
    }
}

按固定顺序加锁,从源头掐断循环等待

这是最有效、成本最低的预防手段。只要所有线程都按同一顺序请求锁(比如总是先

lockA
lockB
),就不可能形成 A→B→A 的环。

给锁对象命名要有含义和顺序感,例如
_lockForOrder
_lockForInventory
,再按业务语义排序(订单优先于库存)
避免在不同方法里“凭感觉”加锁:方法 A 里先锁 B 再锁 A,方法 B 里先锁 A 再锁 B → 死锁高发区 如果必须跨多个资源加锁,建议封装成单一协调锁(如
_globalResourceLock
),或用
ReaderWriterLockSlim
替代多粒度
lock

排查死锁:从 C# 代码到 SQL,分层抓证据

死锁不只发生在 C# 层,常是“C# 线程 + 数据库事务”联合触发。要分层看:

C# 层:启用
ThreadPool.GetAvailableThreads
Thread.CurrentThread.ManagedThreadId
日志,观察线程是否长期卡在某个
lock
WaitOne
调用上
SQL 层:捕获错误号
1205
(死锁受害者),配合
sys.dm_exec_requests
sys.dm_tran_locks
查谁在等什么资源
工具辅助:SQL Server Profiler 或 XEvent 捕获
deadlock graph
,图中箭头方向直接标出“谁在等谁”
关键提示:不要只看异常日志里的“死锁”,要看前后 5 秒内的所有 SQL 执行顺序和锁模式(U、X、S),往往问题出在看似无关的 UPDATE 前置语句上

C# 死锁真正难的不是写对

lock
,而是多个模块协作时没人统一管锁顺序,或者把数据库事务和内存锁混在同一逻辑里——这种耦合一旦形成,单点修复几乎无效。

相关推荐