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.
Il codice sarebbe utile, almeno con la chiamata a Sincronizzazione e pulizia del thread offendente. –