什么是死锁?
死锁(Deadlock) 是一种并发编程中的问题,指两个或多个线程在等待彼此释放资源的情况下进入无限期的等待状态,导致程序无法继续运行。
死锁的必要条件(四个必要条件)
- 互斥(Mutual Exclusion): 至少有一个资源是非共享的,线程独占访问。
- 占用并等待(Hold and Wait): 线程持有某些资源的同时,还在等待其他线程持有的资源。
- 不可抢占(No Preemption): 线程持有的资源不能被强制夺走,必须由线程自己释放。
- 循环等待(Circular Wait): 存在一个线程集合,每个线程都在等待其他线程持有的资源。
只要四个条件同时满足,就有可能发生死锁。
死锁示例
以下代码展示了两个线程试图以不同顺序获取锁,从而导致死锁:
using System;
using System.Threading;
class Program
{
private static readonly object lockA = new object();
private static readonly object lockB = new object();
static void Thread1()
{
lock (lockA)
{
Console.WriteLine("Thread 1 acquired lockA");
Thread.Sleep(1000); // 模拟某种工作
lock (lockB)
{
Console.WriteLine("Thread 1 acquired lockB");
}
}
}
static void Thread2()
{
lock (lockB)
{
Console.WriteLine("Thread 2 acquired lockB");
Thread.Sleep(1000); // 模拟某种工作
lock (lockA)
{
Console.WriteLine("Thread 2 acquired lockA");
}
}
}
static void Main(string[] args)
{
new Thread(Thread1).Start();
new Thread(Thread2).Start();
}
}
结果: 两个线程互相等待对方释放锁,最终陷入死锁。
如何避免死锁?
以下是避免死锁的几种常见方法:
1. 遵循固定的锁定顺序
确保所有线程以相同的顺序获取锁,从而避免循环等待条件。
示例:
static void Thread1()
{
lock (lockA)
{
Console.WriteLine("Thread 1 acquired lockA");
lock (lockB)
{
Console.WriteLine("Thread 1 acquired lockB");
}
}
}
static void Thread2()
{
lock (lockA) // 改变锁的顺序,遵循 lockA -> lockB
{
Console.WriteLine("Thread 2 acquired lockA");
lock (lockB)
{
Console.WriteLine("Thread 2 acquired lockB");
}
}
}
2. 尝试锁(TryLock)
使用超时机制避免线程无限期等待。例如,使用 Monitor.TryEnter
尝试获取锁,如果超时则放弃。
示例:
static void TryLockExample()
{
if (Monitor.TryEnter(lockA, TimeSpan.FromSeconds(1)))
{
try
{
Console.WriteLine("LockA acquired");
if (Monitor.TryEnter(lockB, TimeSpan.FromSeconds(1)))
{
try
{
Console.WriteLine("LockB acquired");
}
finally
{
Monitor.Exit(lockB);
}
}
else
{
Console.WriteLine("Failed to acquire lockB, avoiding deadlock");
}
}
finally
{
Monitor.Exit(lockA);
}
}
else
{
Console.WriteLine("Failed to acquire lockA, avoiding deadlock");
}
}
3. 使用锁的层次化管理
引入一个锁管理机制,根据资源优先级分配锁,避免多个线程争抢资源。
4. 避免占用并等待
在请求锁之前,尽量释放当前持有的锁或确保一次性获取所有所需的资源。
5. 使用并发库中的高级工具
现代 C# 提供了一些线程安全的集合(如 ConcurrentDictionary
、BlockingCollection
)和并发工具(如 SemaphoreSlim
、Task
等),这些工具能在一定程度上减少手动管理锁的需求。
6. 检测和恢复
- 检测: 使用工具检测是否有死锁风险。
- 恢复: 在代码中添加检测逻辑,当发现死锁可能性时,强制释放锁或终止线程。
总结
- 死锁的关键原因: 线程之间的循环等待和资源争抢。
- 避免方法:
- 确保获取锁的顺序一致。
- 使用超时或尝试锁定的方式避免无限等待。
- 使用线程安全的高级工具减少显式锁的使用。
- 在设计上避免资源互相依赖的情况。
通过良好的设计和工具的合理使用,可以有效降低死锁发生的概率,确保并发程序的稳定性和性能。