在多线程编程中,死锁(Deadlock)是一种非常常见的问题,通常发生在两个或多个线程相互等待对方持有的锁,导致它们都无法继续执行。要避免死锁,需要了解死锁的四个必要条件以及相应的解决策略。
死锁的形成
死锁是指两个或多个线程相互等待对方释放资源,导致所有线程都无法继续执行。典型的死锁场景如下:
1.线程1拥有资源A,并在等待资源B;
2.线程2拥有资源B,并在等待资源A;
3.两者互相等待对方释放资源,形成了一个循环依赖,导致所有线程都被永久阻塞。
死锁的四个必要条件:
1.互斥条件(Mutex Exclusion):资源一次只能被一个线程占用。
2.持有并等待条件(Hold and Wait):线程持有一个资源并等待获取其他资源。
3.不可剥夺条件(No Preemption):线程已获得的资源条件不能被强行剥夺,只能由线程自己释放。
4.循环等待条件(Circular Wait):存在一组线程,每个线程都在等待下一线程持有的资源,形成一个环形等待。
如果满足以上四个条件,死锁就有可能发生。因此,解决死锁的关键是打破这些条件之一。
避免死锁的策略
1.锁的顺序(Ordering Locks)
确保所有线程按相同的顺序请求锁。这可以打破死锁的循环等待条件。只要所有的线程都以相同的顺序请求资源,死锁就不会发生。
例如,假设有两个锁lock1和lock2,我们确保所有线程总是先获取lock1,然后后获取lock2,避免死锁。
示例:按顺序获取以避免死锁
private static readonly object _lock1 = new object();
private static readonly object _lock2 = new object();
public void Thread1Work()
{
lock(_lock1)//线程1先获取锁1
{
Console.WriteLine("Thread 1 acquired lock1");
Thread.Sleep(100);
lock(_lock2)//线程1然后获取锁2
{
Console.WriteLine("Thread 1 acquired lock2");
}
}
}
public void Thread2Work()
{
lock (_lock1) // 线程2必须按相同顺序获取锁1
{
Console.WriteLine("Thread 2 acquired lock1");
Thread.Sleep(100);
lock (_lock2) // 线程2然后获取锁2
{
Console.WriteLine("Thread 2 acquired lock2");
}
}
}
如何避免死锁:
• 锁的顺序:通过让线程按照相同的顺序获取锁,可以避免互相等待对方释放锁的问题。
• 结果:Thread1和Thread2都会先获取lock1,然后获取lock2,不会形成死锁。
2.锁的超时机制(Timeout)
使用超时机制来获取锁,如果某个线程在等待锁时超时,则可以放弃操作并避免死锁。这样可以打破持有并等待条件。
示例:使用超时检测潜在的死锁
private static readonly object _lock1 = new object(); // 锁对象1
private static readonly object _lock2 = new object(); // 锁对象2
public void Thread1Work()
{
if (Monitor.TryEnter(_lock1, TimeSpan.FromSeconds(1))) // 尝试获取锁1,超时时间1秒
{
try
{
Console.WriteLine("Thread 1 acquired lock1");
// 模拟线程1需要额外的时间处理一些事情
Thread.Sleep(100);
if (Monitor.TryEnter(_lock2, TimeSpan.FromSeconds(1))) // 尝试获取锁2,超时时间1秒
{
try
{
Console.WriteLine("Thread 1 acquired lock2");
}
finally
{
Monitor.Exit(_lock2); // 释放锁2
}
}
else
{
Console.WriteLine("Thread 1 failed to acquire lock2, potential deadlock detected.");
}
}
finally
{
Monitor.Exit(_lock1); // 释放锁1
}
}
else
{
Console.WriteLine("Thread 1 failed to acquire lock1, potential deadlock detected.");
}
}
public void Thread2Work()
{
if (Monitor.TryEnter(_lock2, TimeSpan.FromSeconds(1))) // 尝试获取锁2,超时时间1秒
{
try
{
Console.WriteLine("Thread 2 acquired lock2");
// 模拟线程2需要额外的时间处理一些事情
Thread.Sleep(100);
if (Monitor.TryEnter(_lock1, TimeSpan.FromSeconds(1))) // 尝试获取锁1,超时时间1秒
{
try
{
Console.WriteLine("Thread 2 acquired lock1");
}
finally
{
Monitor.Exit(_lock1); // 释放锁1
}
}
else
{
Console.WriteLine("Thread 2 failed to acquire lock1, potential deadlock detected.");
}
}
finally
{
Monitor.Exit(_lock2); // 释放锁2
}
}
else
{
Console.WriteLine("Thread 2 failed to acquire lock2, potential deadlock detected.");
}
}
如何避免死锁:
• 锁的超时机制:使用Monitor.TryEnter 来设置获取锁的超时时间。如果超过指定时间无法获取锁,线程可以退出或执行其他操作。
• 结果:如果一个线程未能在指定时间内获取锁,它将放弃尝试并避免陷入死锁。
3.减少锁的持有时间(Minimize Lock Scope)
尽量缩短线程持有锁的时间,减少锁的竞争,进而降低死锁的可能性。只在需要访问共享资源的最小范围内加锁,防止不必要的锁定。
示例:缩短锁的持有时间
private static readonly object _lock1 = new object(); // 锁对象1
private static int _sharedResource = 0; // 共享资源
public void IncrementResource()
{
// 仅在需要访问共享资源时获取锁
lock (_lock1)
{
_sharedResource++; // 修改共享资源
Console.WriteLine($"Resource incremented to {_sharedResource} by Thread {Thread.CurrentThread.ManagedThreadId}");
}
// 锁在这里就释放了,不会在其他不必要的代码块中持有
DoOtherWork(); // 其他操作不需要锁定
}
private void DoOtherWork()
{
// 执行其他不需要锁定的任务
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} is doing other work.");
}
如何避免死锁:
• 减少锁的持有时间:尽量缩小锁定的范围,确保只有在修改共享资源时才持有锁。这样可以减少锁竞争,降低死锁发生的机率。
• 结果:线程在修改完共享资源后立刻释放锁,从而使其他线程有机会获得锁。
总结:
死锁形成的条件:
1.互斥条件:每次只有一个线程能够访问资源。
2.持有并等待条件:线程已经持有一个资源,并在等待其他资源。
3.不可剥夺条件:线程持有的资源不能被强行剥夺,必须由线程自己释放。
4.循环等待条件:一组线程形成循环,每个线程都在等待下一个线程释放资源。
避免死锁的策略:
1.锁的顺序:确保所有线程按照相同的顺序获取锁,避免循环等待。
2.锁的超时机制:使用Monitor.TryEnter等等待超时的锁机制,避免无期限等待锁。
3.减少锁的持有时间:尽量缩小锁定范围,减少锁竞争,降低死锁的可能性。
标签:Console,Thread,C#,lock2,死锁,lock1,线程,Deadlock From: https://blog.csdn.net/qq_73220363/article/details/143111833