2010-03-25 193 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);),以執行額外的初始化。