È meglio usare TThread's & # 8220; Synchronize & # 8221; o utilizzare i messaggi di Windows per IPC tra thread principale e secondario?

StackOverflow https://stackoverflow.com/questions/1806339

  •  05-07-2019
  •  | 
  •  

Domanda

Ho un'applicazione GUI VCL multi-thread piuttosto semplice scritta con Delphi 2007. Faccio un po 'di elaborazione in più thread figlio (fino a 16 concorrenti) che devono aggiornare un controllo della griglia sul mio modulo principale (semplicemente inserendo le stringhe in un griglia). Nessuno dei thread secondari si parla mai tra di loro.

Il mio progetto iniziale consisteva nel chiamare TThread's " Synchronize " per aggiornare il modulo di controllo della griglia all'interno del thread attualmente in esecuzione. Tuttavia, capisco che chiamare Synchronize essenzialmente viene eseguito come se fosse il thread principale quando chiamato. Con un massimo di 16 thread in esecuzione contemporaneamente (e la maggior parte dell'elaborazione del thread figlio richiede da < 1 secondo a ~ 10 secondi) Window Messages sarebbe un progetto migliore?

Ho funzionato a questo punto in cui il thread figlio pubblica un messaggio di Windows (costituito da un record di più stringhe) e il thread principale ha un ascoltatore e aggiorna semplicemente la griglia quando viene ricevuto un messaggio.

Qualche opinione sul metodo migliore per IPC in questa situazione? Messaggi finestra o "Sincronizza"?

Se uso i messaggi della finestra, mi suggerisci di racchiudere il codice in cui inserisco la griglia in un blocco TCriticalSection (entra e esci)? O non dovrò preoccuparmi della sicurezza del thread poiché sto scrivendo sulla griglia nel thread principale (sebbene all'interno della funzione del gestore dei messaggi della finestra)?

È stato utile?

Soluzione

Modifica

Sembra che molti dei dettagli di implementazione siano cambiati da Delphi 4 e 5 (le versioni Delphi che sto ancora usando per la maggior parte del mio lavoro) e Allen Bauer ha commentato quanto segue:

  

Da D6, TThread non utilizza più SendMessage. Utilizza una coda di lavoro thread-safe in cui " lavoro " previsto per il thread principale è posizionato. Un messaggio viene inviato al thread principale per indicare che il lavoro è disponibile e il thread in background si blocca su un evento. Quando il ciclo di messaggi principale sta per diventare inattivo, chiama " CheckSynchronize " per vedere se qualche lavoro è in attesa. Se è così, lo elabora. Una volta completato un elemento di lavoro, l'evento in cui il thread in background è bloccato viene impostato per indicare il completamento. Introdotto nel time frame D2006, è stato aggiunto il metodo TThread.Queue che non si blocca.

Grazie per la correzione. Quindi prendi i dettagli nella risposta originale con un granello di sale.

Ma questo non influisce davvero sui punti fondamentali. Continuo a sostenere che l'idea di Synchronize () è fatalmente imperfetta, e questo sarà ovvio nel momento in cui si tenta di mantenere occupati diversi nuclei di una macchina moderna. Non " sincronizzare " i tuoi thread, lasciali lavorare fino al termine. Cerca di ridurre al minimo tutte le dipendenze tra di loro. Soprattutto quando si aggiorna la GUI non vi è assolutamente nessun motivo per attendere il completamento. Se Synchronize () utilizza SendMessage () o PostMessage () , il blocco stradale risultante è lo stesso.


Quello che presenti qui non è affatto un'alternativa, poiché Synchronize () utilizza SendMessage () internamente. Quindi è più una questione con quale arma vuoi usare per spararti al piede.

Synchronize () è stato con noi sin dall'introduzione di TThread in Delphi 2 VCL, il che è davvero un peccato perché è una delle maggiori disfunzioni del design in il VCL.

Come funziona? Utilizza una chiamata SendMessage () a una finestra creata nel thread principale e imposta i parametri del messaggio per passare l'indirizzo di un metodo di oggetto senza parametri da chiamare. Poiché i messaggi di Windows verranno elaborati solo nel thread che ha creato la finestra di destinazione ed eseguirà il suo ciclo di messaggi, questo sospenderà il thread, gestirà il messaggio nel contesto del thread VCL principale, chiamerà il metodo e riprenderà il thread solo dopo il metodo ha terminato l'esecuzione.

Quindi cosa c'è che non va (e cosa c'è di simile nell'usare direttamente SendMessage () )? Diverse cose:

  • Forzare qualsiasi thread ad eseguire codice nel contesto di un altro thread forza due switch di contesto del thread, che bruciano inutilmente i cicli della CPU.
  • Mentre il thread VCL elabora il messaggio per chiamare il metodo sincronizzato, non può elaborare nessun altro messaggio.
  • Quando più di un thread utilizza questo metodo, tutto bloccherà e attenderà il ritorno di Synchronize () o SendMessage () . Questo crea un enorme collo di bottiglia.
  • C'è un deadlock in attesa di accadere. Se il thread chiama Synchronize () o SendMessage () mentre si tiene un oggetto di sincronizzazione e il thread VCL durante l'elaborazione del messaggio deve acquisire lo stesso oggetto di sincronizzazione, l'applicazione verrà bloccata up.
  • Lo stesso si può dire delle chiamate API in attesa dell'handle del thread: l'utilizzo di WaitForSingleObject () o WaitForMultipleObjects () senza alcun mezzo per elaborare i messaggi causerà un deadlock se il thread ha bisogno di questi modi per "sincronizzare" con l'altro thread.

Quindi cosa usare invece? Diverse opzioni, ne descriverò alcune:

  • Usa PostMessage () invece di SendMessage () (o PostThreadMessage () se i due thread non sono entrambi VCL filo). È importante tuttavia non utilizzare alcun dato nei parametri del messaggio che non sarà più valido quando il messaggio arriva, poiché il thread di invio e ricezione non sono affatto sincronizzati, quindi è necessario utilizzare altri mezzi per assicurarsi che qualsiasi stringa , il riferimento all'oggetto o il pezzo di memoria sono ancora validi quando il messaggio viene elaborato, anche se il thread di invio potrebbe non esistere più.

  • Crea strutture di dati thread-safe, inserisci i dati dai thread di lavoro e consumali dal thread principale. Usa PostMessage () solo per avvisare il thread VCL che i nuovi dati sono arrivati ??per essere elaborati, ma non pubblicare messaggi ogni volta. Se hai un flusso continuo di dati potresti persino avere il poll thread VCL per i dati (forse usando un timer), ma questa è solo una versione da uomo povero.

  • Non utilizzare più gli strumenti di basso livello. Se sei almeno su Delphi 2007, scarica OmniThreadLibrary e inizia a pensare in termini di attività, non di discussioni . Questa libreria ha molte funzionalità per lo scambio di dati tra thread e sincronizzazione. Ha anche un'implementazione del pool di thread, il che è una buona cosa: quanti thread dovresti usare non dipendono solo dall'applicazione ma anche dall'hardware su cui è in esecuzione, quindi molte decisioni possono essere prese solo in fase di runtime. OTL ti consentirà di eseguire attività su un thread del pool di thread, in modo che il sistema possa ottimizzare il numero di thread simultanei in fase di esecuzione.

Modifica

Rileggendo, mi rendo conto che non intendi utilizzare SendMessage () ma PostMessage () - beh, alcune delle precedenti non si applicano quindi , ma lo lascerò sul posto. Tuttavia, ci sono altri punti nella tua domanda che voglio affrontare:

  

Con un massimo di 16 thread in esecuzione contemporaneamente (e la maggior parte dell'elaborazione del thread figlio richiede da < 1 secondo a ~ 10 secondi) i Messaggi di Windows sarebbero un design migliore?

Se pubblichi un messaggio da ogni thread una volta ogni secondo o anche un periodo più lungo, il design va bene. Quello che non dovresti fare è pubblicare centinaia o più messaggi al thread al secondo, perché la coda dei messaggi di Windows ha una lunghezza finita e i messaggi personalizzati non dovrebbero interferire troppo con la normale elaborazione dei messaggi (il tuo programma inizierà a non rispondere).

  

dove il thread figlio pubblica un messaggio di Windows (costituito da un record di più stringhe)

Un messaggio di finestra non può contenere un record. Presenta due parametri, uno di tipo WPARAM , l'altro di tipo LPARAM . Puoi solo lanciare un puntatore a tale record su uno di questi tipi, quindi la durata della registrazione deve essere gestita in qualche modo. Se lo alloci in modo dinamico, devi anche liberarlo, il che è soggetto a errori. Se passi un puntatore a un record nello stack o a un campo oggetto, devi assicurarti che sia ancora valido quando il messaggio viene elaborato, il che è più difficile per i messaggi pubblicati che per i messaggi inviati.

  

mi suggerisce di racchiudere il codice in cui inserisco la griglia in un blocco TCriticalSection (invio e uscita)? O non dovrò preoccuparmi della sicurezza del thread poiché sto scrivendo sulla griglia nel thread principale (sebbene all'interno della funzione del gestore dei messaggi della finestra)?

Non è necessario farlo, poiché la chiamata PostMessage () ritornerà immediatamente, quindi non è necessaria alcuna sincronizzazione a questo punto . Dovrai sicuramente preoccuparti della sicurezza del thread, sfortunatamente non puoi sapere quando . Devi assicurarti che l'accesso ai dati sia thread-safe, sempre bloccando i dati per l'accesso, usando oggetti di sincronizzazione. Non esiste davvero un modo per raggiungerlo per i record, è sempre possibile accedere direttamente ai dati.

Altri suggerimenti

A proposito, puoi anche usare TThread.Queue ( ) anziché TThread. Sincronizza () . Queue () è la versione asincrona, non blocca il thread chiamante:

( Queue è disponibile da D8).

Preferisco Synchronize () o Queue () , perché è molto più facile da capire (per altri programmatori) e OO migliore rispetto all'invio semplice di messaggi (senza controllo su di esso o in grado di eseguire il debug!)

Anche se sono sicuro che esiste una strada giusta e una sbagliata. Ho scritto codice usando entrambi i metodi e quello a cui continuo a tornare è il metodo SendMessage e non sono sicuro del perché.

L'uso di SendMessage vs Synchronize non fa davvero alcuna differenza. Entrambi funzionano essenzialmente allo stesso modo. Penso che il motivo per cui continuo a utilizzare SendMessage è che percepisco un maggiore controllo, ma non lo so.

La routine SendMessage fa sospendere il thread chiamante e attende fino a quando la finestra di destinazione termina l'elaborazione del messaggio inviato. Per questo motivo il thread dell'app principale viene sostanzialmente sincronizzato con il thread figlio chiamante per la durata della chiamata. Non è necessario utilizzare una sezione critica nel gestore dei messaggi di Windows.

Il trasferimento dei dati è essenzialmente unidirezionale, dal thread chiamante al thread dell'applicazione principale. È possibile restituire un valore di tipo intero in message.result ma nulla che punti a un oggetto memoria nel thread principale.

Poiché i due thread sono " sincronizzati " quel punto e il thread delle app principali sono attualmente collegati in risposta al SendMessage, quindi non devi preoccuparti che altri thread entrino e distruggano i tuoi dati allo stesso tempo. Quindi non devi preoccuparti di usare le sezioni critiche o altri tipi di misure di sicurezza del thread.

Per cose semplici puoi definire un singolo messaggio (wm_threadmsg1) e usare i campi wparam e lparam per trasferire avanti e indietro i messaggi di stato (intero). Per esempi più complessi è possibile passare una stringa passandola tramite lparam e riconducendola a un longint. A-la longint (pchar (myvar)) o usa pwidechar se usi D2009 o più recente.

Se lo hai già fatto funzionare con i metodi di sincronizzazione, non mi preoccuperei di rielaborarlo per apportare una modifica.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top