Delphi:通过报告正在运行的线程的调用堆栈来调试关键部分挂起问题,以便在锁定“失败”时进行排查。

9
我正在寻找一种方法来调试一个罕见的Delphi 7关键段(TCriticalSection)挂起/死锁。在这种情况下,如果一个线程等待超过10秒钟的关键段,我希望产生一份报告,其中包含当前锁定关键段的线程和无法在等待10秒后锁定关键段的线程的堆栈跟踪。如果引发异常或应用程序终止,则可以接受。
如果可能的话,我更喜欢继续使用关键段而不是其他同步原语,但如果必要的话,我可以切换(例如,以获得超时功能)。
如果工具/方法能够在IDE之外的运行时工作,那就更好了,因为这很难按需复制死锁。在我能够在IDE中重现死锁的罕见情况下,如果我尝试暂停以开始调试,IDE只会坐在那里什么也不做,并且永远无法到达可以查看线程或调用堆栈的状态。但我可以重置正在运行的程序。
更新:在这种情况下,我只处理一个关键段和2个线程,因此这可能不是锁定顺序问题。我相信有一次不正确的嵌套尝试跨两个不同的线程进入锁定,导致死锁。
5个回答

9
你应该创建并使用自己的锁对象类。它可以使用关键段或互斥体来实现,具体取决于您是否想要进行调试。
创建自己的类有一个额外的好处:您可以实现锁定层次结构,并在违反时引发异常。当锁不是每次都按相同顺序获取时,就会发生死锁。为每个锁分配一个锁级别可以检查锁是否按正确顺序获取。您可以将当前锁级别存储在线程变量中,并允许仅获取具有较低锁级别的锁,否则将引发异常。这将捕获所有违规行为,即使没有死锁发生,因此应该大大加快调试速度。
至于获取线程的堆栈跟踪信息,Stack Overflow 上有很多相关问题。
更新:
你写道:
在这种情况下,我只处理一个关键段和两个线程,所以这可能不是锁排序问题。我认为存在一种不合适的嵌套尝试跨越两个不同线程进入锁,导致死锁。

这不可能是整个故事的全部。在Windows上,使用单个关键部分和两个线程会导致死锁是不可能的,因为线程可以递归地获取关键部分。一定还有另一个阻塞机制参与其中,例如SendMessage()调用。

但是,如果您真的只涉及两个线程,那么其中一个必须是主线程/VCL/GUI线程。在这种情况下,您应该能够使用MadExcept "主线程冻结检查"功能。它将尝试向主线程发送消息,并在经过可自定义的时间而未处理消息时失败。如果您的主线程在关键部分上被阻塞,而另一个线程在消息处理调用上被阻塞,则MadExcept应该能够捕获此并为两个线程提供堆栈跟踪。


madExcept也可以在任何时候被要求进行线程转储,因此可能非常适合这种情况。 - mj2008
madExcept看起来是最好的选择。谢谢! - Anagoge

4

这并不是对你问题的直接回答,而是最近我遇到的一个问题,让我和几个同事困惑了一段时间。

它是一个间歇性的线程挂起,涉及一个关键部分。一旦我们知道了原因,就非常明显,并给我们所有人带来了一个“噢”时刻。然而,要找到它确实需要一些严肃的搜索(添加越来越多的跟踪日志以确定有问题的语句),这就是为什么我想提一下它。

它也是在进入关键部分时发生的。另一个线程确实获得了该关键部分。死锁似乎不是原因,因为只涉及一个关键部分,所以在不同的顺序中获取锁不会出现问题。持有关键部分的线程应该继续然后释放锁,允许其他线程获取它。

最终结果表明,持有锁的线程最终正在访问(如果我没记错)组合框的ItemIndex,看起来相当无害。不幸的是,获取该ItemIndex依赖于消息处理。而等待锁的线程是主应用程序线程……(以防万一有人想知道:主线程处理所有消息……)

如果一开始它涉及VCL更明显,我们可能会更早地想到这一点。然而,它是在非UI相关的代码中开始的,只有在沿着调用树和返回所有触发事件及其处理程序直到UI代码的仪器(输入 - 输出跟踪)之后才显现出VCL的参与。

希望这个故事能对面临神秘挂起问题的人有所帮助。


3

使用互斥量代替临界区。互斥量和临界区之间有一点不同——临界区更有效,而互斥量更灵活。例如,在调试版本中可以轻松地在互斥量和临界区之间切换。

对于临界区,我们使用:

var
  FLock: TRTLCriticalSection;

  InitializeCriticalSection(FLock);  // create lock
  DeleteCriticalSection(FLock);      // free lock
  EnterCriticalSection(FLock);       // acquire lock
  LeaveCriticalSection(FLock);       // release lock

与互斥锁相同:

var FLock: THandle;

  FLock:= CreateMutex(nil, False, nil);  // create lock
  CloseHandle(FLock);                    // free lock
  WaitForSingleObject(FLock, Timeout);   // acquire lock
  ReleaseMutex(FLock);                   // release lock

您可以通过实现如下的获取锁函数,使用超时(以毫秒为单位;10秒为10000)来控制互斥锁:

function AcquireLock(Lock: THandle; TimeOut: LongWord): Boolean;
begin
  Result:= WaitForSingleObject(Lock, Timeout) = WAIT_OBJECT_0;
end;

2

您也可以使用 TryEnterCriticalSection API 与关键段一起使用,而不是使用 EnterCriticalSection

如果您使用 TryEnterCriticalSection 并且锁定获取失败,则该 API 返回 False,您可以以任何您认为合适的方式处理失败,而不仅仅是锁定线程。

类似于:

while not TryEnterCriticalSection(fLock) and (additional_checks) do
begin
  deal_with_failure();
  sleep(500); // wait 500 ms
end;

请注意,Delphi的TCriticalSection使用EnterCriticalSection,因此除非您调整该类,否则您将不得不自己创建类或处理关键部分的初始化/去初始化。

1
如果你想要在等待某个东西时设置超时,你可以尝试用 TEvent 信号来替换你的关键部分。你可以说等待事件,指定一个超时长度,并检查结果代码。如果该信号被设置,则可以继续执行。如果没有,说明它超时了,那么你可以引发异常。

至少我是这样在 D2010 中做的。不确定 Delphi 7 是否有 TEvent,但很可能会有。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接