C Sharp 死锁预防
外观
死锁(Deadlock)是多线程编程中常见的问题,指两个或多个线程因互相等待对方持有的资源而无限阻塞的状态。在C#并发编程中,死锁可能导致程序完全停止响应,因此理解其成因和预防方法至关重要。
死锁的必要条件[编辑 | 编辑源代码]
死锁的发生需同时满足以下四个条件(由Edsger Dijkstra提出):
- 互斥条件:资源一次只能被一个线程占用。
- 占有并等待:线程持有资源的同时请求其他资源。
- 非抢占条件:已分配的资源不能被强制剥夺。
- 循环等待:线程间形成环形等待链。
死锁示例[编辑 | 编辑源代码]
以下是一个典型的C#死锁代码示例:
object lockA = new object();
object lockB = new object();
void Thread1Work()
{
lock (lockA)
{
Thread.Sleep(100);
lock (lockB)
{
Console.WriteLine("Thread1 completed");
}
}
}
void Thread2Work()
{
lock (lockB)
{
Thread.Sleep(100);
lock (lockA)
{
Console.WriteLine("Thread2 completed");
}
}
}
// 启动线程
Thread t1 = new Thread(Thread1Work);
Thread t2 = new Thread(Thread2Work);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
输出结果:程序无限挂起,无任何输出。
死锁预防策略[编辑 | 编辑源代码]
1. 锁顺序一致性[编辑 | 编辑源代码]
强制所有线程以相同的顺序获取锁。修改上例:
void Thread1Work()
{
lock (lockA)
{
Thread.Sleep(100);
lock (lockB)
{
Console.WriteLine("Thread1 completed");
}
}
}
void Thread2Work()
{
lock (lockA) // 改为先请求lockA
{
Thread.Sleep(100);
lock (lockB)
{
Console.WriteLine("Thread2 completed");
}
}
}
2. 使用超时机制[编辑 | 编辑源代码]
通过 Monitor.TryEnter
设置超时:
bool lockTakenA = false;
bool lockTakenB = false;
try
{
Monitor.TryEnter(lockA, 500, ref lockTakenA);
if (lockTakenA)
{
Monitor.TryEnter(lockB, 500, ref lockTakenB);
if (lockTakenB)
{
// 执行操作
}
}
}
finally
{
if (lockTakenB) Monitor.Exit(lockB);
if (lockTakenA) Monitor.Exit(lockA);
}
3. 减少锁粒度[编辑 | 编辑源代码]
使用更细粒度的锁或无锁数据结构(如 ConcurrentQueue
)。
4. 避免嵌套锁[编辑 | 编辑源代码]
重构代码以减少锁的嵌套层级。
实际案例:银行转账[编辑 | 编辑源代码]
模拟银行账户转账时的死锁场景:
class Account
{
public decimal Balance;
private readonly object balanceLock = new object();
public void Transfer(Account target, decimal amount)
{
lock (balanceLock)
{
lock (target.balanceLock)
{
Balance -= amount;
target.Balance += amount;
}
}
}
}
问题:若线程1执行A→B转账,线程2执行B→A转账,可能死锁。 解决方案:按账户ID顺序加锁。
数学建模[编辑 | 编辑源代码]
死锁概率与线程数()和资源数()相关:
总结[编辑 | 编辑源代码]
- 死锁的四个必要条件缺一不可。
- 预防核心是破坏至少一个条件(通常选择「循环等待」或「占有并等待」)。
- 实际开发中应优先使用高级并发工具(如
async/await
、Task
、ConcurrentBag
等)。