9

我正在寻找一种方法来调试罕见的 Delphi 7 关键部分 (TCriticalSection) 挂起/死锁。在这种情况下,如果一个线程在关键部分等待超过 10 秒,我想生成一个报告,其中包含当前锁定关键部分的线程以及未能能够的线程的堆栈跟踪等待 10 秒后锁定临界区。如果引发异常或应用程序终止,则可以。

如果可能的话,我宁愿继续使用临界区,而不是使用其他同步原语,但可以在必要时切换(例如获得超时功能)。

如果该工具/方法在 IDE 之外的运行时工作,这是一个好处,因为这很难按需重现。在极少数情况下,我可以在 IDE 中复制死锁,如果我尝试暂停以开始调试,IDE 只是坐在那里什么都不做,并且永远不会进入我可以查看线程或调用堆栈的状态。不过,我可以重置正在运行的程序。

更新:在这种情况下,我只处理一个关键部分和 2 个线程,所以这可能不是锁排序问题。我相信有一个不正确的嵌套尝试跨两个不同的线程进入锁,这会导致死锁。

4

5 回答 5

9

您应该创建和使用自己的锁对象类。它可以使用临界区或互斥锁来实现,具体取决于您是否要调试它。

创建自己的类还有一个额外的好处:您可以实现锁定层次结构并在违反它时引发异常。当锁不是每次都以完全相同的顺序获取时,就会发生死锁。为每个锁分配一个锁级别可以检查是否以正确的顺序获取锁。您可以将当前锁定级别存储在线程变量中,并且只允许使用具有较低锁定级别的锁定,否则您会引发异常。即使没有发生死锁,这也会捕获所有违规行为,因此它应该会大大加快您的调试速度。

至于获取线程的堆栈跟踪,这里有很多关于 Stack Overflow 处理这个问题的问题。

更新

你写:

在这种情况下,我只处理一个关键部分和 2 个线程,所以这可能不是锁排序问题。我相信有一个不正确的嵌套尝试跨两个不同的线程进入锁,这会导致死锁。

这不可能是全部。没有办法在 Windows 上单独使用两个线程和一个临界区来死锁,因为临界区可以通过线程在那里递归地获取。必须涉及另一种阻塞机制,例如SendMessage()调用。

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

于 2010-09-15T17:52:42.560 回答
4

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

这是一个间歇性的线程挂起,涉及一个关键部分,一旦我们知道了原因,它就非常明显了,给了我们所有人一个“d'oh”的时刻。然而,它确实需要一些认真的搜寻(添加越来越多的跟踪日志来查明有问题的语句),这就是为什么我想我会提到它。

它也是在一个关键部分进入。另一个线程确实获得了该关键部分。死锁本身似乎不是原因,因为只涉及一个关键部分,因此以不同的顺序获取锁可能没有问题。持有临界区的线程应该简单地继续,然后释放锁,允许其他线程获取它。

最后结果证明持有锁的线程最终访问的是(IIRC)组合框的ItemIndex,看起来相当无害。不幸的是,获取 ItemIndex 依赖于消息处理。等待锁的线程是主应用程序线程......(以防万一有人想知道:主线程完成所有消息处理......)

如果从一开始就更明显地涉及到 vcl,我们可能会更早地想到这一点。但是,它始于非 ui 相关代码,并且 vcl 参与仅在沿调用树添加检测(进入 - 退出跟踪)并返回所有触发事件及其处理程序直到 ui 代码后才变得明显。

只是希望这个故事对面临神秘悬案的人有所帮助。

于 2010-09-15T18:35:32.180 回答
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

您可以通过实现这样的获取锁定功能来使用互斥锁的超时(以毫秒为单位;10000 持续 10 秒):

function AcquireLock(Lock: THandle; TimeOut: LongWord): Boolean;
begin
  Result:= WaitForSingleObject(Lock, Timeout) = WAIT_OBJECT_0;
end;
于 2010-09-15T21:56:49.833 回答
2

您还可以将关键部分与TryEnterCriticalSectionAPI 一起使用,而不是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,因此除非您调整该类,否则您将不得不自己编写类,否则您将不得不处理关键部分的初始化/取消初始化。

于 2011-11-22T21:01:26.757 回答
1

如果您希望能够等待超时,您可以尝试用 TEvent 信号替换您的关键部分。你可以说等待事件,给它一个超时长度,然后检查结果代码。如果信号已设置,则可以继续。如果不是,如果它超时,你会引发一个异常。

至少,这就是我在 D2010 中的做法。我不确定 Delphi 7 是否有 TEvent,但它可能有。

于 2010-09-15T17:44:52.743 回答