2010-03-25 150 views
14

我在销毁某些线程时有时会遇到死锁问题。我试图调试这个问题,但是在IDE中进行调试时似乎永远不会存在死锁,这可能是因为IDE中的事件速度很慢。Delphi线程死锁

问题:

主应用程序启动时主线程创建了几个线程。线程始终处于活动状态并与主线程同步。没有问题。当应用程序结束时(mainform.onclose)线程被破坏如下:

thread1.terminate; 
thread1.waitfor; 
thread1.free; 

等等。

但有时其中一个线程(使用同步将某些字符串记录到备忘录中)会在关闭时锁定整个应用程序。当我调用waitform和harmaggeddon时,我怀疑这个线程正在同步,但这只是一个猜测,因为调试时永远不会发生死锁(或者我从来没有能够重现它)。有什么建议?

+1

代码会很有帮助,至少在您调用同步和清理违规线程时会很有帮助。 – 2010-03-25 11:40:14

回答

27

记录消息只是其中Synchronize()根本没有任何意义的那些区域之一。您应该改为创建一个日志目标对象,该对象具有受关键部分保护的字符串列表,并向其添加日志消息。让主VCL线程从该列表中删除日志消息,并在日志窗口中显示它们。这有几个优点:

  • 你不需要拨打Synchronize(),这是一个坏主意。好的副作用是你的关机问题消失。

  • 工作线程可以继续他们的工作,而不会阻塞主线程事件处理或尝试记录消息的其他线程。

  • 由于可以一次将多条消息添加到日志窗口,因此性能会提高。如果你使用BeginUpdate()EndUpdate()这会加快速度。

我可以看到没有什么缺点 - 日志消息的顺序也被保留下来。

编辑:

我会添加一些更多的信息和一些代码一起玩,为了说明,有更好的方法做你需要做的事情。

调用Synchronize()来自与VCL程序中的主应用程序线程不同的线程将导致调用线程阻塞,传递的代码将在VCL线程的上下文中执行,然后调用线程将被解除阻塞并且继续运行。在单处理器的时代,这可能是一个好主意,无论如何,一次只能运行一个线程,但对于多处理器或内核来说,这是一个巨大浪费,应该不惜一切代价避免。如果在8核心机器上有8个工作线程,那么将它们调用Synchronize()可能会将吞吐量限制为可能的一小部分。

其实,调用Synchronize()从来不是一个好主意,因为它会导致死锁。有一个更有说服力的理由不使用它,永远。

使用PostMessage()发送日志消息会照顾僵局问题的,但它有其自身的问题:

  • 每个日志字符串将导致公布和处理的消息,引起了许多开销。无法一次处理多个日志消息。

  • Windows消息只能在参数中携带机器字大小的数据。因此发送字符串是不可能的。在字符串转到PChar之后发送字符串是不安全的,因为字符串在处理消息时可能已被释放。分配工作线程中的内存并在处理完消息后释放VCL线程中的内存是一种解决方法。一种增加更多开销的方式。

  • Windows中的消息队列具有有限的大小。发布过多的消息可能会导致队列变满并且丢弃消息。这不是一件好事,并且与之前的观点一起导致内存泄漏。

  • 在生成任何计时器或绘图消息之前,将处理队列中的所有消息。源源不断的大量信息可能会导致程序无响应。

收集日志信息的数据结构看起来是这样的:

type 
    TLogTarget = class(TObject) 
    private 
    fCritSect: TCriticalSection; 
    fMsgs: TStrings; 
    public 
    constructor Create; 
    destructor Destroy; override; 

    procedure GetLoggedMsgs(AMsgs: TStrings); 
    procedure LogMessage(const AMsg: string); 
    end; 

constructor TLogTarget.Create; 
begin 
    inherited; 
    fCritSect := TCriticalSection.Create; 
    fMsgs := TStringList.Create; 
end; 

destructor TLogTarget.Destroy; 
begin 
    fMsgs.Free; 
    fCritSect.Free; 
    inherited; 
end; 

procedure TLogTarget.GetLoggedMsgs(AMsgs: TStrings); 
begin 
    if AMsgs <> nil then begin 
    fCritSect.Enter; 
    try 
     AMsgs.Assign(fMsgs); 
     fMsgs.Clear; 
    finally 
     fCritSect.Leave; 
    end; 
    end; 
end; 

procedure TLogTarget.LogMessage(const AMsg: string); 
begin 
    fCritSect.Enter; 
    try 
    fMsgs.Add(AMsg); 
    finally 
    fCritSect.Leave; 
    end; 
end; 

许多线程可以调用LogMessage()同时,进入临界区将连续访问列表,并加入他们的消息后,线程可以继续他们的工作。

这留下了一个问题:VCL线程如何知道何时调用GetLoggedMsgs()从对象中移除消息并将它们添加到窗口中。一个穷人的版本将有一个计时器和民意调查。更好的办法是打电话PostMessage()添加日志消息时:

procedure TLogTarget.LogMessage(const AMsg: string); 
begin 
    fCritSect.Enter; 
    try 
    fMsgs.Add(AMsg); 
    PostMessage(fNotificationHandle, WM_USER, 0, 0); 
    finally 
    fCritSect.Leave; 
    end; 
end; 

这仍然有太多的发布的消息的问题。一条消息只需在上一条消息被处理时发布:

procedure TLogTarget.LogMessage(const AMsg: string); 
begin 
    fCritSect.Enter; 
    try 
    fMsgs.Add(AMsg); 
    if InterlockedExchange(fMessagePosted, 1) = 0 then 
     PostMessage(fNotificationHandle, WM_USER, 0, 0); 
    finally 
    fCritSect.Leave; 
    end; 
end; 

虽然这仍然可以改进。使用计时器可以解决发布的消息填满队列的问题。下面是一个小的类,它实现这一点:

type 
    TMainThreadNotification = class(TObject) 
    private 
    fNotificationMsg: Cardinal; 
    fNotificationRequest: integer; 
    fNotificationWnd: HWND; 
    fOnNotify: TNotifyEvent; 
    procedure DoNotify; 
    procedure NotificationWndMethod(var AMsg: TMessage); 
    public 
    constructor Create; 
    destructor Destroy; override; 

    procedure RequestNotification; 
    public 
    property OnNotify: TNotifyEvent read fOnNotify write fOnNotify; 
    end; 

constructor TMainThreadNotification.Create; 
begin 
    inherited Create; 
    fNotificationMsg := RegisterWindowMessage('thrd_notification_msg'); 
    fNotificationRequest := -1; 
    fNotificationWnd := AllocateHWnd(NotificationWndMethod); 
end; 

destructor TMainThreadNotification.Destroy; 
begin 
    if IsWindow(fNotificationWnd) then 
    DeallocateHWnd(fNotificationWnd); 
    inherited Destroy; 
end; 

procedure TMainThreadNotification.DoNotify; 
begin 
    if Assigned(fOnNotify) then 
    fOnNotify(Self); 
end; 

procedure TMainThreadNotification.NotificationWndMethod(var AMsg: TMessage); 
begin 
    if AMsg.Msg = fNotificationMsg then begin 
    SetTimer(fNotificationWnd, 42, 10, nil); 
    // set to 0, so no new message will be posted 
    InterlockedExchange(fNotificationRequest, 0); 
    DoNotify; 
    AMsg.Result := 1; 
    end else if AMsg.Msg = WM_TIMER then begin 
    if InterlockedExchange(fNotificationRequest, 0) = 0 then begin 
     // set to -1, so new message can be posted 
     InterlockedExchange(fNotificationRequest, -1); 
     // and kill timer 
     KillTimer(fNotificationWnd, 42); 
    end else begin 
     // new notifications have been requested - keep timer enabled 
     DoNotify; 
    end; 
    AMsg.Result := 1; 
    end else begin 
    with AMsg do 
     Result := DefWindowProc(fNotificationWnd, Msg, WParam, LParam); 
    end; 
end; 

procedure TMainThreadNotification.RequestNotification; 
begin 
    if IsWindow(fNotificationWnd) then begin 
    if InterlockedIncrement(fNotificationRequest) = 0 then 
    PostMessage(fNotificationWnd, fNotificationMsg, 0, 0); 
    end; 
end; 

的类的实例可以被添加到TLogTarget,调用在主线程通知事件,但每秒最多几十倍。

+1

+ NUM_ALLOWED_VOTES :)这是一篇很棒的文章! – jpfollenius 2010-03-26 08:00:29

+0

什么时候应该叫'RequestNotification'? – EProgrammerNotFound 2013-11-26 14:26:14

+0

@MatheusFreitas:只要不是主线程的线程需要主线程在其上下文中执行某些操作。在日志系统的情况下,工作线程中添加的日志消息需要在GUI中显示。如果工作线程调用'RequestNotification()',则它可以继续工作,同时确保在将来的某个时间GUI线程将被通知新的日志消息并显示它。我希望这足够清楚了吗? – mghie 2013-11-26 14:32:53

2

将互斥对象添加到主线程。尝试关闭表单时获取互斥体。在其他线程中,在处理序列中同步之前检查互斥锁。

7

考虑用PostMessage的呼叫替换Synchronize,并在表单中处理此消息以向备注添加日志消息。沿着线的东西:(把它当作伪代码)

WM_LOG = WM_USER + 1; 
... 
MyForm = class (TForm) 
    procedure LogHandler (var Msg : Tmessage); message WM_LOG; 
end; 
... 
PostMessage (Application.MainForm.Handle, WM_LOG, 0, PChar (LogStr)); 

这避免了两个线程等待对方的一切僵局问题。

编辑(感谢Serg的提示):请注意,以所描述的方式传递字符串是不安全的,因为字符串可能会在VCL线程使用它之前销毁。正如我提到的 - 这只是为了伪代码。

+1

错了。如果接收线程的消息循环不会处理消息,比如当它在'WaitForSingleObject()'内部,等待线程终止,那么'SendMessage()'将会阻塞。 – mghie 2010-03-25 12:09:38

+1

您应该使用PostMessage而不是SendMessage来避免线程阻塞。 – kludg 2010-03-25 12:24:00

+0

对,我把这两个弄糊涂了。谢谢@Serg提供更具建设性的批评。 – jpfollenius 2010-03-25 12:41:57

1

很简单:

TMyThread = class(TThread) 
protected 
    FIsIdle: boolean; 
    procedure Execute; override; 
    procedure MyMethod; 
public 
    property IsIdle : boolean read FIsIdle write FIsIdle; //you should use critical section to read/write it 
end; 

procedure TMyThread.Execute; 
begin 
    try 
    while not Terminated do 
    begin 
     Synchronize(MyMethod); 
     Sleep(100); 
    end; 
    finally 
    IsIdle := true; 
    end; 
end; 

//thread destroy; 
lMyThread.Terminate; 
while not lMyThread.IsIdle do 
begin 
    CheckSynchronize; 
    Sleep(50); 
end; 
+1

对于仅写入一次的布尔值,不需要使用关键部分。 – mghie 2010-03-27 10:36:46

0

Delphi的TThread类对象(和继承类)破坏时已经调用WaitFor的,但它取决于你是否创建了CreateSuspended与否的线程。如果您在调用第一个Resume之前使用CreateSuspended = true执行额外的初始化,则应考虑创建自己的构造函数(调用inherited Create(false);),以执行额外的初始化。