跳转到内容

C Sharp 死锁预防

来自代码酷

死锁(Deadlock)是多线程编程中常见的问题,指两个或多个线程因互相等待对方持有的资源而无限阻塞的状态。在C#并发编程中,死锁可能导致程序完全停止响应,因此理解其成因和预防方法至关重要。

死锁的必要条件[编辑 | 编辑源代码]

死锁的发生需同时满足以下四个条件(由Edsger Dijkstra提出):

  1. 互斥条件:资源一次只能被一个线程占用。
  2. 占有并等待:线程持有资源的同时请求其他资源。
  3. 非抢占条件:已分配的资源不能被强制剥夺。
  4. 循环等待:线程间形成环形等待链。

graph LR A[线程1: 持有锁A, 请求锁B] --> B[线程2: 持有锁B, 请求锁A] B --> A

死锁示例[编辑 | 编辑源代码]

以下是一个典型的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顺序加锁。

数学建模[编辑 | 编辑源代码]

死锁概率与线程数(n)和资源数(m)相关: Pdeadlockn!mn

总结[编辑 | 编辑源代码]

  • 死锁的四个必要条件缺一不可。
  • 预防核心是破坏至少一个条件(通常选择「循环等待」或「占有并等待」)。
  • 实际开发中应优先使用高级并发工具(如 async/awaitTaskConcurrentBag 等)。