2010-03-25 9 views
14

A volte ho un problema con un deadlock quando si distruggono alcuni thread. Ho provato a eseguire il debug del problema ma il deadlock non sembra mai esistere durante il debug in IDE, forse a causa della bassa velocità degli eventi nell'IDE.deadlock thread Delphi

Il problema:

Il filo principale crea più thread all'avvio dell'applicazione. I thread sono sempre attivi e sincronizzati con il thread principale. Nessun problema. I thread vengono distrutti quando l'applicazione termina (mainform.onclose) in questo modo:

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

e così via.

Ma a volte uno dei thread (che registra una stringa in un memo, utilizzando la sincronizzazione) bloccherà l'intera applicazione alla chiusura. Ho il sospetto che il thread si sta sincronizzando quando chiamo waitform e l'harmaggeddon accade, ma questo è solo un tentativo perché il deadlock non avviene mai durante il debug (o non sono mai stato in grado di riprodurlo comunque). Qualche consiglio?

+1

Il codice sarebbe utile, almeno con la chiamata a Sincronizzazione e pulizia del thread offendente. –

risposta

27

I messaggi di registrazione sono solo una di quelle aree in cui Synchronize() non ha alcun senso. Dovresti invece creare un oggetto di destinazione del registro, che ha un elenco di stringhe, protetto da una sezione critica e aggiungervi i messaggi di registro. Chiedere al thread VCL principale di rimuovere i messaggi di registro da tale elenco e visualizzarli nella finestra del registro. Questo ha diversi vantaggi:

  • Non è necessario chiamare Synchronize(), che è solo una cattiva idea. Un piacevole effetto collaterale è che il tuo tipo di problemi di spegnimento scompare.

  • I thread di lavoro possono continuare il lavoro senza bloccare la gestione degli eventi del thread principale o altri thread che tentano di registrare un messaggio.

  • Le prestazioni aumentano, poiché è possibile aggiungere più messaggi alla finestra di registro in una volta sola. Se usi BeginUpdate() e EndUpdate() questo accelera le cose.

Non ci sono svantaggi che riesco a vedere, anche l'ordine dei messaggi di registro viene mantenuto.

Edit:

vorrei aggiungere qualche informazione in più e un po 'di codice per giocare, per illustrare che ci sono modi molto migliori per fare ciò che è necessario fare.

Chiamare Synchronize() da un thread diverso dal thread dell'applicazione principale in un programma VCL causerà il blocco del thread chiamante, il codice passato da eseguire nel contesto del thread VCL e quindi il thread chiamante verrà sbloccato e continuare a correre. Potrebbe essere stata una buona idea ai tempi delle macchine a processore singolo, su cui può essere eseguito solo un thread alla volta, ma con più processori o core è un enorme spreco e dovrebbe essere evitato a tutti i costi. Se hai 8 thread di lavoro su un 8 core, farli chiamare Synchronize() probabilmente limiterà il throughput a una frazione di ciò che è possibile.

In realtà, chiamare Synchronize() non è mai stata una buona idea, in quanto può portare a deadlock. Un motivo in più convincente per non usarlo, mai.

Utilizzando PostMessage() per inviare i messaggi di log si occuperà della questione stallo, ma ha i suoi problemi:

  • Ogni stringa di registro causerà un messaggio per essere pubblicato e trattati, causando molta testa. Non c'è modo di gestire più messaggi di log in una volta sola.

  • I messaggi di Windows possono trasportare solo i dati della parola macchina nei parametri. L'invio di stringhe è quindi impossibile. L'invio di stringhe dopo un typecast a PChar non è sicuro, poiché la stringa potrebbe essere stata liberata al momento dell'elaborazione del messaggio. L'allocazione della memoria nel thread di lavoro e la liberazione di quella memoria nel thread VCL dopo che il messaggio è stato elaborato è una via d'uscita. Un modo per aggiungere un ulteriore sovraccarico.

  • Le code di messaggi in Windows hanno una dimensione finita. La pubblicazione di troppi messaggi può comportare il pieno riempimento della coda e la caduta dei messaggi. Non è una buona cosa, e insieme al punto precedente porta a perdite di memoria.

  • Tutti i messaggi nella coda verranno elaborati prima che venga generato qualsiasi timer o messaggio di colore. Un flusso costante di molti messaggi inviati può quindi causare la mancata risposta del programma.

Una struttura dati che raccoglie i messaggi di log potrebbe essere la seguente:

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; 

Molti fili possono chiamare LogMessage() contemporaneamente, entrare nella sezione critica si serializzare accesso alla lista, e dopo l'aggiunta loro messaggio del le discussioni possono continuare con il loro lavoro.

Ciò lascia la domanda su come il thread VCL sa quando chiamare GetLoggedMsgs() per rimuovere i messaggi dall'oggetto e aggiungerli alla finestra. La versione di un povero sarebbe avere un timer e un sondaggio. Un modo migliore sarebbe quello di chiamare PostMessage() quando viene aggiunto un messaggio di registro:

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

Questo ha ancora il problema con troppi inviato messaggi.È necessario inviare un messaggio solo quando è stato elaborato il precedente:

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; 

Tuttavia, ciò può essere ancora migliorato. L'uso di un timer risolve il problema dei messaggi postati che riempiono la coda. Quanto segue è una piccola classe che implementa questa:

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; 

un'istanza della classe può essere aggiunto a TLogTarget, per chiamare un evento di notifica nel thread principale, ma al massimo qualche decina di volte al secondo.

+1

+ NUM_ALLOWED_VOTES :) questa è una buona scrittura! – jpfollenius

+0

Quando devo chiamare 'RequestNotification'? – EProgrammerNotFound

+0

@MatheusFreitas: ogni volta che un thread che non è il thread principale ha bisogno del thread principale per fare qualcosa nel suo contesto. Nel caso del sistema di registrazione, un messaggio di registro aggiunto in un thread di lavoro deve essere visualizzato nella GUI. Se il thread worker chiama 'RequestNotification()', può continuare il suo lavoro assicurandosi che in futuro il thread della GUI verrà avvisato del nuovo messaggio di log e visualizzato. Spero che sia abbastanza chiaro? – mghie

2

Aggiungi oggetto mutex al thread principale. Ottieni mutex quando prova la forma vicina. In altro thread controlla il mutex prima di sincronizzarlo nella sequenza di elaborazione.

7

Considerare la sostituzione di Synchronize con una chiamata a PostMessage e gestire questo messaggio nel modulo per aggiungere un messaggio di registro al memo. Qualcosa sulla falsariga di: (prendere come pseudo-codice)

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)); 

che evita tutti i problemi stallo di due thread in attesa per l'altro.

EDIT (Grazie a Serg per il suggerimento): Si noti che passare la stringa nel modo descritto non è sicuro poiché la stringa potrebbe essere distrutta prima che il thread VCL lo utilizzi. Come ho già detto, questo era inteso solo come pseudocodice.

+1

Sbagliato. 'SendMessage()' bloccherà lo stesso se il loop del messaggio del thread ricevente non gestirà il messaggio, come quando è dentro 'WaitForSingleObject()', in attesa che il thread termini. – mghie

+1

È necessario utilizzare PostMessage anziché SendMessage per evitare il blocco del thread. – kludg

+0

Giusto, ho confuso i due. Grazie a @Serg per le critiche più costruttive. – jpfollenius

1

E 'semplice:

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

Non è necessario utilizzare le sezioni critiche per un booleano che viene scritto solo una volta. – mghie

0

oggetto TThread di Delphi (e classi che ereditano) chiama già WaitFor quando distruggere, ma dipende dal fatto che si è creato il filo con CreateSuspended o no. Se si utilizza CreateSuspended = true per eseguire l'inizializzazione aggiuntiva prima di chiamare il primo CV, è consigliabile creare il proprio costruttore (chiamando inherited Create(false);) che esegue l'inizializzazione aggiuntiva.