线程死锁的定义
线程死锁是指两个或多个线程在争夺资源时,彼此形成了循环等待,导致它们都无法继续执行的现象。简单来说,线程 A 持有资源 1,并等待资源 2,而线程 B 持有资源 2,并等待资源 1。因为相互等待,这些线程永远无法完成。
死锁发生的四个必要条件
根据操作系统中的死锁理论,死锁的产生需要满足以下四个条件:
- 互斥条件(Mutual Exclusion)
至少有一个资源是不能被共享的,某一时刻只能由一个线程占用。 - 占有并等待条件(Hold and Wait)
一个线程持有至少一个资源,并等待其他资源的释放。 - 不可剥夺条件(No Preemption)
已获得的资源不能被强制剥夺,只能由占有它的线程主动释放。 - 循环等待条件(Circular Wait)
存在线程组成的循环链,每个线程都在等待下一个线程所持有的资源。
如果满足以上四个条件,就可能发生死锁。
死锁的示例代码
以下是一个简单的死锁场景代码,用 C# 展示:
class Program
{
private static object lockA = new object();
private static object lockB = new object();
static void Main()
{
Thread thread1 = new Thread(Thread1);
Thread thread2 = new Thread(Thread2);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
}
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");
}
}
}
}
在以上代码中:
- Thread1 先锁住 lockA,然后尝试锁住 lockB。
- Thread2 先锁住 lockB,然后尝试锁住 lockA。
- 由于线程间相互等待对方释放资源,导致死锁。
避免死锁的几种方法
- 按顺序加锁(Lock Ordering)
确保所有线程按照相同的顺序申请锁资源。例如,确保 Thread1 和 Thread2 都先锁住 lockA,再锁住 lockB。 - 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 (lockA) { Console.WriteLine("Thread 2 acquired lockA"); Thread.Sleep(1000); lock (lockB) { Console.WriteLine("Thread 2 acquired lockB"); } } }
- 尝试锁(Try Locking)
使用 Monitor.TryEnter 方法尝试获取锁。如果在超时时间内无法获得锁,则释放当前已持有的锁,避免死锁。 - static void Thread1() { if (Monitor.TryEnter(lockA, TimeSpan.FromSeconds(1))) { try { Console.WriteLine("Thread 1 acquired lockA"); if (Monitor.TryEnter(lockB, TimeSpan.FromSeconds(1))) { try { Console.WriteLine("Thread 1 acquired lockB"); } finally { Monitor.Exit(lockB); } } } finally { Monitor.Exit(lockA); } } else { Console.WriteLine("Thread 1 could not acquire lockA"); } }
- 使用超时机制
通过 Task 或 CancellationToken 实现超时机制,避免线程无限等待。 - 避免嵌套锁定
避免一个线程在持有一个锁的同时再去申请另一个锁。 - 采用死锁检测工具
使用第三方工具或者框架对死锁进行检测,例如 Visual Studio 的调试器可以帮助发现死锁。 - 使用并发集合
使用 C# 中的线程安全集合(如 ConcurrentDictionary)来代替手动管理的锁。
总结
- 死锁的发生主要是由于多个线程之间的资源相互依赖。
- 避免死锁的关键是打破死锁的四个条件之一,例如按顺序加锁、使用尝试锁或者避免嵌套锁定。
- 在设计多线程程序时,应尽量减少锁的使用范围,降低锁的复杂性,提高程序的健壮性。