多线程编程中死锁问题的跟踪与解决

多线程编程中,由于需要同步对象的访问,稍有不慎,就可能造成死锁。而线程死锁的跟踪调试分析是一件很让人纠葛的事情,如何跟踪死锁是一件很让人闹心的事。在Vista/Server2008及以后的版本,Windows提供了OpenThreadWaitChainSession等一系列函数(Wait Chain Traversal,WCT)可以获取同步对象的等待状态,如果在IDE调试时出现问题,Delphi的Threads可以看到相应的同步对象等待状态及各个线程的堆栈,但这种问题往往需要在特定情况下才能出现,调试很困难。而在运行时,由于Delphi和C++ Builder并没有提供相应的封装,造成我们很难跟踪出问题时的状态。为解决这一问题,QDAC为大家提供QMapSymbols单元以提供相关的支持。

首先,我们要检测是否发生死锁。

方法一:检测死锁一种是人为检测,在发生死锁时,如果是主线程与后台线程发冲突,程序会停止响应。如果是后台线程之间发生死锁,则后台相关的作业会停止执行,程序通过日志等途径可以查看到相关的状态,从而确定是发生了死锁。

方法二:通过后台线程检测是否发生死锁。通过开启一个后台线程,使用WCT函数检查是否发生死锁。

下一步,发生死锁时,我们要做两件事。

一、想办法记录各个线程的堆栈信息到日志,以便进一步跟踪问题发生的来源,找到冲突的代码。

二、通过WCT函数找到出问题的对象类型和发生冲突的线程,并将其信息记录到日志,结合上面记录的线程堆栈信息,就可以明确的找到出现死锁问题的堆栈调用。

最后一步,就是根据日志中的堆栈信息,修改程序的代码,避免冲突的发生。

QMapSymbols目前封装了WCT函数并提供了几个简单的封装:

1、通过EnumWaitChains函数可以方便的列举出各个线程的等待状态;如果出现死锁,会直接打印输出相应的线程的堆栈信息。

2、StackByThreadId/StackByThreadHandle/StackOfThread三种重载提供了不同的获取线程堆栈的途径,程序随时可以通过这三个函数查询任意线程(包含调用线程自身)的堆栈信息。这一组函数不需要Windows Vista/2008以上操作系统支持。

3、程序可以调用函数EnableDeadlockCheck来启用死锁检测。

前面说了在Vista/Server 2008以后的版本才提供了WCT函数,那么,如果在早期的操作系统上,我们如何来跟踪死锁的问题呢?

首先,检测死锁的办法我们就不得不做一些改变。在发生死锁时,除了人为检测外,由于冲突的线程的堆栈会被处于同步等待状态而无法继续执行,我们可以检查线程的当前执行的指令寄存器的值是否在一段时间内发生变化,来做为是否发生死锁的判定依据。

接下来在发生死锁时,一样可以通过堆栈来找到线程冲突的代码,只是无法直观的判断发生死锁的同步对象类型,但一般到这一步,找到各个线程堆栈后,要找到出错的代码位置并不难,然后结合上下文分析,一样可以便捷的定位出冲突位置并做出必要的修正。

【示例】

为了演示死锁及等待队列的效果,我们首先创建了一个线程类,这个线程类尝试先后进入两个临界。

procedure TDeadLockThread.Execute;
begin
inherited;
FCS1.Enter; 
Sleep(10);
FCS2.Enter;
end;

而后,我们创建两个不同的线程,并分别将两个临界对象以不同的顺序赋给线程,以便模拟死锁冲突:

  CS1 := TCriticalSection.Create;
  CS2 := TCriticalSection.Create;
  // 避免线程先进入
  CS1.Enter;
  CS2.Enter;
  AThread1 := TDeadLockThread.Create(True);
  AThread1.FCS1 := CS1;
  AThread1.FCS2 := CS2;
  AThread1.Resume;
  AThread2 := TDeadLockThread.Create(True);
  AThread2.FCS1 := CS2;
  AThread2.FCS2 := CS1;
  AThread2.Resume;
  CS1.Leave;
  CS2.Leave;
  Sleep(100);

现在,死锁已经被触发了,由于两个线程分别请求进入对方已经进入的临界,从而引发了死锁。此时,我们调用 EnumWaitChains函数,就会得到当前各个线程的等待状态,简单的ShowMessage显示下:

ShowMessage(EnumWaitChains);

deadlockwaitchain

如果我们在程序调用EnableDeadlockCheck函数,则程序会自动生成一个日志,记录死锁情况,下面是一个具体上面的例子的结果:

线程 5144
 !!!已死锁!!! 
 线程 5144 <运行中> 
[堆栈]
ntdll.dll.76F2CD7C
ntdll.dll.76F0EB9F
ntdll.dll.76F0EBCD
0050E2F6 System.SyncObjs.pas[1023]:System.SyncObjs.TCriticalSection.Acquire
004C0B22 System.Classes.pas[14161]:System.Classes.ThreadProc
0040919C System.pas[23671]:System.ThreadWrapper
74D39199 C:\WINDOWS\SYSTEM32\KERNEL32.DLL:GetCurrentThread
ntdll.dll.76F3A22B
ntdll.dll.76F3A201

 CriticalSection <等待中> 
 线程 7364 <运行中> 
[堆栈]
ntdll.dll.76F2CD7C
ntdll.dll.76F0EB9F
ntdll.dll.76F0EBCD
0050E2F6 System.SyncObjs.pas[1023]:System.SyncObjs.TCriticalSection.Acquire
004C0B22 System.Classes.pas[14161]:System.Classes.ThreadProc
0040919C System.pas[23671]:System.ThreadWrapper
74D39199 C:\WINDOWS\SYSTEM32\KERNEL32.DLL:GetCurrentThread
ntdll.dll.76F3A22B
ntdll.dll.76F3A201

 CriticalSection <等待中> 
 线程 5144 <运行中> 
[堆栈]
ntdll.dll.76F2CD7C
ntdll.dll.76F0EB9F
ntdll.dll.76F0EBCD
0050E2F6 System.SyncObjs.pas[1023]:System.SyncObjs.TCriticalSection.Acquire
004C0B22 System.Classes.pas[14161]:System.Classes.ThreadProc
0040919C System.pas[23671]:System.ThreadWrapper
74D39199 C:\WINDOWS\SYSTEM32\KERNEL32.DLL:GetCurrentThread
ntdll.dll.76F3A22B
ntdll.dll.76F3A201

线程 7364
 !!!已死锁!!! 
 线程 7364 <运行中> 
[堆栈]
ntdll.dll.76F2CD7C
ntdll.dll.76F0EB9F
ntdll.dll.76F0EBCD
0050E2F6 System.SyncObjs.pas[1023]:System.SyncObjs.TCriticalSection.Acquire
004C0B22 System.Classes.pas[14161]:System.Classes.ThreadProc
0040919C System.pas[23671]:System.ThreadWrapper
74D39199 C:\WINDOWS\SYSTEM32\KERNEL32.DLL:GetCurrentThread
ntdll.dll.76F3A22B
ntdll.dll.76F3A201

 CriticalSection <等待中> 
 线程 5144 <运行中> 
[堆栈]
ntdll.dll.76F2CD7C
ntdll.dll.76F0EB9F
ntdll.dll.76F0EBCD
0050E2F6 System.SyncObjs.pas[1023]:System.SyncObjs.TCriticalSection.Acquire
004C0B22 System.Classes.pas[14161]:System.Classes.ThreadProc
0040919C System.pas[23671]:System.ThreadWrapper
74D39199 C:\WINDOWS\SYSTEM32\KERNEL32.DLL:GetCurrentThread
ntdll.dll.76F3A22B
ntdll.dll.76F3A201

 CriticalSection <等待中> 
 线程 7364 <运行中> 
[堆栈]
ntdll.dll.76F2CD7C
ntdll.dll.76F0EB9F
ntdll.dll.76F0EBCD
0050E2F6 System.SyncObjs.pas[1023]:System.SyncObjs.TCriticalSection.Acquire
004C0B22 System.Classes.pas[14161]:System.Classes.ThreadProc
0040919C System.pas[23671]:System.ThreadWrapper
74D39199 C:\WINDOWS\SYSTEM32\KERNEL32.DLL:GetCurrentThread
ntdll.dll.76F3A22B
ntdll.dll.76F3A201

 

很显然,从堆栈信息我们已经可以很容易的发现冲突的位置了,然后跟踪代码找到原因即可。

分享到: