临界区死锁和死循环

本文详细介绍了Windows中使用CRITICAL_SECTION实现线程同步的临界区机制,包括其内部结构和字段的含义,并通过经典死锁案例展示了线程等待临界区的情况。此外,还提供了查找死循环的调试步骤,帮助开发者理解和解决多线程同步问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.临界区

目录

1.临界区

2. 经典死锁案例:

3.查找死循环案例


使用CRITICAL_SECTION 结构来保证不会有多个线程重入被保护的代码段
实现在用户态的同步机制,相对内核对象来说,开销更小
适用于同步同一进程内的多个线程

如:
CRITICAL_SECTION g_cs;

EnterCriticalSection(&g_cs);
//以原子方式访问共享资源
..... //do something
LeaveCriticalSection(&g_cs);

RTL_CRITICAL_SECTION 结构如下:
struct RTL_CRITICAL_SECTION
{
    PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
    LONG LockCount;
    LONG RecursionCount;
    HANDLE OwningThread;
    HANDLE LockSemaphore;
    ULONG_PTR SpinCount;
};

LockCount字段,初始值为-1,被线程拥有后,大于等于0,反映等待和已经进入关键区的线程数RecursionCount字段,此字段包含所有者线程已经获得该临界区的次数。如果该数值为零,下一个尝试获取该临界区的线程将会成功。
OwningThread字段,拥有线程(已经进入临界区)的句柄。
LockSemaphore字段,唤醒等待的线程在初始化临界区结构时,系统会分配一个RTL_CRITICAL_SECTION_DEBUG结构,每个临界区的调试结构依靠ProcessLocksList字段相互联系在一起。
SpinCount 仅用于多处理器系统。MSDN文档对此字段进行如下说明:“在多处理器系统中,如果该临界区不可用,调用线程将在对与该临界区相关的信号执行等待操作之前,旋转 dwSpinCount 次。如果该临界区在旋转操作期间变为可用,该调用线程就避免了等待操作。”旋转计数可以在多处理器计算机上提供更佳性能,其原因在于在一个循环中旋转通常要快于进入内核模式等待状态。此字段默认值为零,但可以用 InitializeCriticalSectionAndSpinCount API 将其设置为一个不同值。
临界区的调试支持,RTL_CRITICAL_SECTION_DEBUG结构如下:

struct _RTL_CRITICAL_SECTION_DEBUG
{
    WORD   Type;
    WORD   CreatorBackTraceIndex;
    RTL_CRITICAL_SECTION *CriticalSection;
    LIST_ENTRY ProcessLocksList;
    DWORD EntryCount;
    DWORD ContentionCount;
    DWORD Spare[ 2 ];
}

这一结构由 InitializeCriticalSection 分配和初始化。它既可以由 NTDLL内的预分配数组分配,也可以由进程堆分配。RTL_CRITICAL_SECTION的这一伴随结构包含一组匹配字段,具有迥然不同的角色:有两个难以理解,随后两个提供了理解这一临界区链结构的关键,两个是重复设置的,最后两个未使用。

下面是对 RTL_CRITICAL_SECTION 字段的说明。
Type 此字段未使用,被初始化为数值 0。
CreatorBackTraceIndex 此字段仅用于诊断情形中。在注册表项
HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File ExecutionOptions\YourProgram 之下是 keyfield、GlobalFlag 和 StackTraceDatabaseSizeInMb值。注意,只有在运行稍后说明的 Gflags 命令时才会显示这些值。这些注册表值的设置正确时,CreatorBackTraceIndex
字段将由堆栈跟踪中所用的一个索引值填充。在 MSDN 中搜索 GFlags 文档中的短语“create user mode stack trace database”和“enlarging the user-mode stack trace database”,可以找到有关这一内容的更多信息。
CriticalSection 指向与此结构相关的 RTL_CRITICAL_SECTION。
图1 说明该基础结构以及 RTL_CRITICAL_SECTION、RTL_CRITICAL_SECTION_DEBUG 和事件链中其他参与者之间的关系。

图 1 临界区的关系

ProcessLocksList LIST_ENTRY 是用于表示双向链表中节点的标准 Windows数据结构。RTL_CRITICAL_SECTION_DEBUG 包含了链表的一部分,允许向前和向后遍历该临界区。
EntryCount/ContentionCount 这些字段在相同的时间、出于相同的原因被递增。这是那些因为不能马上获得临界区而进入等待状态的线程的数目。与 LockCount 和 RecursionCount字段不同,这些字段永远都不会递减。
Spares这两个字段未使用,甚至未被初始化(尽管在删除临界区结构时将这些字段进行了清零)。
即使 RTL_CRITICAL_SECTION_DEBUG中包含多个字段,它也是常规临界区结构的必要成分。事实上,如果系统恰巧不能由进程堆中获得这一结构的存储区,InitializeCriticalSection
将返回为 STATUS_NO_MEMORY 的 LastError 结果,然后返回处于不完整状态的临界区结构。

2. 经典死锁案例:

启动windbg 附加MulThrds.exe

·1.~*  查看所有线程

2.~* k 查看所有线程堆栈

发现有几个线程都在等待进入同一个临界区

3.切换其中某一个线程,查看堆栈,如切到换10号线程

4.查看临界区 !cs 0040b32c

5.查看死锁的临界区,刚好也是0040b32c

拥有的线程是0x00002e14,查看所有线程,发现0x00002e14 已经不在了。

3.查找死循环案例

启动windbg 附加MulThrds.exe

1.查看线程运行时间

~*e .ttime;.echo*****

 2.输出线程tid

~*e .echo **********;? @$tid; .ttime;

 3.切换线程

~~[4040]s

 查看堆栈信息:

参考 : 软件调试  ----张银奎

MulThrds.exe 及源码请查看软件调试

### Java 中处理死锁的解决方案 #### 一、理解死锁产生的条件 为了有效地防止解决死锁问题,首先要了解其形成的四个必要条件:互斥条件、请求与保持条件、不可剥夺条件以及循环等待条件。这些条件共同作用下才会引发死锁现象[^2]。 #### 二、检测死锁的存在 JVM提供了内置工具来辅助诊断死锁情况。通过`jstack`命令可以查看当前运行的应用程序中是否存在潜在的死锁定状态,并获取详细的线程堆栈信息以便进一步分析。此外,在开发阶段也可以利用可视化调试器监控线程活动状况,及时发现异常行为模式[^1]。 #### 三、预防措施 ##### 1. 避免嵌套加锁 尽可能减少在一个方法内部多次申请不同对象上的同步锁的情况;如果确实不可避免,则应确保所有地方按照相同的顺序获得相应资源访问权限。 ```java // 不推荐的做法 - 可能引起死锁 synchronized (resourceA) { synchronized (resourceB) { ... } } // 推荐做法 - 统一加锁顺序 private static final Object lockOrder = new Object(); ... synchronized(lockOrder){ // 对 resourceA resourceB 的操作 } ``` ##### 2. 使用超时机制尝试获取锁 对于那些可能长时间持有而不释放的关键区域,考虑采用带有时间限制的方式去竞争进入临界区的机会。这样即使遇到对方永不退出的情形也能安全返回而不是无限期阻塞下去。 ```java try{ if (!lock.tryLock(5, TimeUnit.SECONDS)) { System.out.println("未能成功取得锁"); return; } }catch(InterruptedException e){ Thread.currentThread().interrupt(); }finally{ lock.unlock(); } ``` ##### 3. 应用高级并发结构替代传统锁 诸如读写锁(`ReentrantReadWriteLock`)、信号量(Semaphore)等更灵活的数据共享控制手段往往能够更好地满足实际需求的同时降低陷入僵局的风险程度。 ```java final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void read() throws InterruptedException { rwl.readLock().lockInterruptibly(); try { // 执行读取逻辑 } finally { rwl.readLock().unlock(); } } void write() throws InterruptedException { rwl.writeLock().lockInterruptibly(); try { // 执行写入逻辑 } finally { rwl.writeLock().unlock(); } } ``` #### 四、修复已发生的死锁 一旦确认存在死锁问题之后,最直接有效的办法就是重启受影响的服务实例让一切恢复正常运作。当然在此之前最好先备份好重要数据以防万一丢失造成更大损失。另外还可以借助于一些第三方库如DeadlockLlama来进行自动化的探测与恢复工作[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值