2026/6/10 18:55:00
网站建设
项目流程
做企业网站需要做什么,wordpress 下一篇,小程序代理加盟政策,漳州 网站设计在 C# / .NET 里#xff0c;死锁的“四个必要条件”其实很好理解#xff0c;把它们看成导致“互相卡住”的四块拼图——四块都存在时#xff0c;才会真的卡死。只要你在设计里打碎其中一块#xff0c;就能避免死锁。
下面逐个讲#xff0c;每个条件都配一段通俗解释和 C# …在 C# / .NET 里死锁的“四个必要条件”其实很好理解把它们看成导致“互相卡住”的四块拼图——四块都存在时才会真的卡死。只要你在设计里打碎其中一块就能避免死锁。下面逐个讲每个条件都配一段通俗解释和 C# 示例。互斥: 某个资源在同一时刻只能被一个线程使用如 lock 锁定的对象持有并等待: 线程已经拿着一个资源还不放手同时又在等待另一个资源。不可抢占:线程持有的资源不能被强制剥夺只能主动释放。循环等待: 线程 A 等待线程 B 的资源线程 B 等待线程 A 的资源形成闭环互斥条件Mutual Exclusion1.1 概念白话版互斥意思是某个资源在同一时刻只能被一个线程使用。类比一间厕所只有一个坑位资源A 进去上厕所时门锁上B 只能在外等厕所就是“互斥资源”。在 C# 中常见的互斥资源包括被 lock 保护的临界区Mutex、Semaphore(1,1) 等文件句柄、数据库连接等有限资源。private readonly object _lockObj new object(); private int _counter 0; public void Increase() { lock (_lockObj) // 这里就是互斥 { // 同一时刻只能有一个线程在这段代码里 _counter; Thread.Sleep(100); // 模拟一些工作 } }lock (_lockObj) 保证 _counter 的操作是“互斥”的如果多个线程同时调用 Increase()会按顺序排队进入 lock 里面。为什么互斥是死锁必要条件如果一个资源可以被多个线程同时使用比如只读资源、纯函数线程之间就不会因为“抢资源”而卡住也就不会形成死锁。占有且等待Hold and Wait2.1 概念白话版占有且等待意思是线程已经拿着一个资源还不放手同时又在等待另一个资源。类比A 拿着钥匙 1资源 1还要再借钥匙 2资源 2才能开第二个房间A 没拿到钥匙 2 之前不会把钥匙 1 还回去此时 A 就是“占有钥匙 1且等待钥匙 2”。2.2 示例private readonly object _lockA new object(); private readonly object _lockB new object(); public void Method1() { lock (_lockA) // 占有资源 A { Thread.Sleep(100); // 模拟业务 lock (_lockB) // 在占有 A 的情况下继续等待 B { // 使用资源 A 和 B 的临界区 } } }在 Method1 中线程先 lock (_lockA)在持有 A 的同时又 lock (_lockB)典型的“占有且等待”。如果同时还有一个方法反过来常见死锁写法public void Method2() { lock (_lockB) // 占有资源 B { Thread.Sleep(100); lock (_lockA) // 在占有 B 的情况下等待 A { // 使用资源 A 和 B 的临界区 } } }当线程 T1 调用 Method1()、线程 T2 调用 Method2() 时就具备了“占有且等待”的条件T1占有 A 等待 BT2占有 B 等待 A如果不存在“占有且等待”会怎样比如设计成线程请求新资源时必须先释放已经持有的资源那么线程不会同时持有多个资源也就不存在资源之间形成“环形依赖”死锁就难以出现。3.不可抢占No Preemption3.1 概念白话版不可抢占意思是线程持有的资源不能被强制剥夺只能主动释放。类比A 进了厕所把门从里面反锁B 再急也不能把门强行踹开只能等 A 出来只有 A 自己愿意开门厕所才会被释放。在 C# 中lock、Monitor、Mutex 等都不支持“强制释放”如果线程挂死在临界区锁就一直不释放。3.2 C# 示例private readonly object _lockObj new object(); public void DoWork() { lock (_lockObj) { // 一旦进来其他线程就必须等待 // 这里如果发生异常或者死循环锁就一直不释放 Thread.Sleep(Timeout.Infinite); // 模拟挂死 } } public void OtherWork() { lock (_lockObj) { // 永远等不到 Console.WriteLine(永远不会到达这里); } }第一个线程进入 DoWork获取 _lockObj 后一直 Sleep模拟挂死第二个线程在 OtherWork 中想获取同一个锁只能永远等待。如果资源可以被抢占会怎样理论上如果系统能“看你占着厕所不干事就强制踢你出来”就能打破死锁但在一般编程语言里这样的强行剥夺很难安全实现所以默认都是不可抢占。循环等待Circular Wait4.1 概念白话版循环等待是死锁最直观的表现形式线程 A 等待线程 B 的资源线程 B 等待线程 A 的资源多个线程之间形成了一个“环形的等待链”。类比A 拿着钥匙 1 等钥匙 2B 拿着钥匙 2 等钥匙 3C 拿着钥匙 3 等钥匙 1A → B → C → A形成一个等待环谁也等不到。两线程最简单的循环等待T1持有 A等待 BT2持有 B等待 A等待关系T1 → B → T2 → A → T1形成环。4.2 C# 示例经典死锁示例private readonly object _lockA new object(); private readonly object _lockB new object(); public void Thread1Work() { lock (_lockA) // T1 持有 A { Console.WriteLine(T1拿到 A准备拿 B...); Thread.Sleep(100); // 让 T2 有时间先拿 B lock (_lockB) // T1 等待 B { Console.WriteLine(T1拿到 B); } } } public void Thread2Work() { lock (_lockB) // T2 持有 B { Console.WriteLine(T2拿到 B准备拿 A...); Thread.Sleep(100); // 让 T1 有时间先拿 A lock (_lockA) // T2 等待 A { Console.WriteLine(T2拿到 A); } } } 启动代码 public void Run() { var t1 new Thread(Thread1Work); var t2 new Thread(Thread2Work); t1.Start(); t2.Start(); t1.Join(); t2.Join(); }执行过程T1 先进入 Thread1Work()获得 _lockAT2 进入 Thread2Work()获得 _lockBT1 在尝试 lock (_lockB) 时被阻塞因为 B 已被 T2 持有T2 在尝试 lock (_lockA) 时被阻塞因为 A 已被 T1 持有T1 等 T2 的 BT2 等 T1 的 A → 构成环形等待。这就是一个完整的死锁四个条件全部满足互斥_lockA、_lockB 都是互斥资源lock占有且等待T1占有 A 等 BT2占有 B 等 A不可抢占A、B 在被持有时不能被强制夺回循环等待T1 等 BT2 持有→ T2 等 AT1 持有→ 形成环。综合示例四个条件如何共同导致死锁我们用一个稍微完整的例子把四个条件串起来看private readonly object _lockA new object(); private readonly object _lockB new object(); public void TaskA() { lock (_lockA) // 1. 互斥获取互斥资源 A { Console.WriteLine(TaskA got A); Thread.Sleep(100); // 2. 占有且等待在持有 A 的状态下继续等待 B lock (_lockB) { Console.WriteLine(TaskA got B); } } } public void TaskB() { lock (_lockB) // 1. 互斥获取互斥资源 B { Console.WriteLine(TaskB got B); Thread.Sleep(100); // 2. 占有且等待在持有 B 的状态下继续等待 A lock (_lockA) { Console.WriteLine(TaskB got A); } } } 运行 public void RunDeadlockDemo() { var tA new Thread(TaskA); var tB new Thread(TaskB); tA.Start(); tB.Start(); tA.Join(); tB.Join(); }四个条件一一对照互斥条件_lockA 和 _lockB 都通过 lock 实现互斥访问。占有且等待TaskA先持有 _lockA然后等待 _lockBTaskB先持有 _lockB然后等待 _lockA。不可抢占一旦 TaskA 拿到 _lockA除非它离开 lock 块否则没有任何办法强制释放同理TaskB 拿到 _lockB 后也只能等它自己释放。循环等待TaskA 等 TaskB 手里的 _lockBTaskB 等 TaskA 手里的 _lockA形成一个闭合等待圈。只要这四个条件同时存在死锁就有可能发生而真正发生死锁时通常就是你没控制好锁的顺序或阻塞方式比如 async 里用 .Result。怎样利用这“四个条件”去避免死锁破坏循环等待统一锁顺序所有地方获取多个锁时规定好顺序比如总是先锁 A 再锁 B不允许有反过来“先 B 后 A”的情况。缓和占有且等待尽量避免持有一个锁时再去申请另一个锁多数业务可以重构为先计算好数据再在短小的临界区一次性拿锁、更新状态。避免不必要的互斥尽量使用不可变对象、多读少写、分区锁等方式减少需要“排队”的资源。增加“抢占”效果实质上是避免永久等待使用 Monitor.TryEnter 超时async 场景用 SemaphoreSlim.WaitAsync CancellationToken超时失败时打日志、回滚操作相当于“主动放弃资源”避免挂死。