Questo è il mio prossimo progetto di test per vedere quale libreria di threading per Delphi mi farebbe meglio per il mio compito di "scansione dei file" che vorrei elaborare in più thread / in un pool di thread.
Per ripetere il mio obiettivo: trasformare la mia "scansione di file" sequenziale di 500-2000 + file dall'approccio non thread a uno thread. Non dovrei avere 500 thread in esecuzione contemporaneamente, quindi vorrei utilizzare un pool di thread. Un pool di thread è una classe simile a una coda che alimenta un numero di thread in esecuzione con l'attività successiva dalla coda.
Il primo tentativo (molto semplice) è stato fatto semplicemente estendendo la classe TThread e implementando il metodo Execute (il mio parser di stringhe di thread).
Poiché Delphi non ha implementato una classe di pool di thread pronta all'uso, nel mio secondo tentativo ho provato a utilizzare OmniThreadLibrary di Primoz Gabrijelcic.
OTL è fantastico, ha milioni di modi per eseguire un'attività in background, una strada da percorrere se si desidera avere un approccio "ignora e dimentica" per consegnare l'esecuzione filettata di parti del codice.
Nota: ciò che segue sarebbe più facile da seguire se si scarica per la prima volta il codice sorgente.
Durante l'esplorazione di altri modi per eseguire alcune delle mie funzioni in modo thread, ho deciso di provare anche l'unità "AsyncCalls.pas" sviluppata da Andreas Hausladen. Andy's AsyncCalls - L'unità di chiamate di funzione asincrona è un'altra libreria che uno sviluppatore Delphi può utilizzare per alleviare il dolore dell'implementazione dell'approccio threaded all'esecuzione di un codice.
Dal blog di Andy: Con AsyncCalls puoi eseguire più funzioni contemporaneamente e sincronizzarle in ogni punto della funzione o del metodo che le ha avviate ... L'unità AsyncCalls offre una varietà di prototipi di funzioni per chiamare funzioni asincrone ... Implementa un pool di thread! L'installazione è semplicissima: basta usare le asincrone da una qualsiasi delle tue unità e hai accesso immediato a cose come "esegui in un thread separato, sincronizza l'interfaccia utente principale, attendi fino al termine".
Oltre alle AsyncCalls gratuite (licenza MPL), Andy pubblica spesso anche le proprie correzioni per l'IDE di Delphi come "Delphi Speed Up" e "DDevExtensions" di cui hai già sentito parlare (se non lo usi già).
In sostanza, tutte le funzioni di AsyncCall restituiscono un'interfaccia IAsyncCall che consente di sincronizzare le funzioni. IAsnycCall espone i seguenti metodi:
//v 2.98 di asynccalls.pas
IAsyncCall = interfaccia
// attende fino al termine della funzione e restituisce il valore restituito
funzione Sync: Integer;
// restituisce True al termine della funzione asincronica
funzione finita: booleana;
// restituisce il valore restituito della funzione asincrona, quando Finished è TRUE
funzione ReturnValue: intero;
// dice ad AsyncCalls che la funzione assegnata non deve essere eseguita nell'attuale threa
procedura ForceDifferentThread;
fine;
Ecco una chiamata di esempio a un metodo che prevede due parametri interi (che restituisce una IAsyncCall):
TAsyncCalls.Invoke (AsyncMethod, i, Random (500));
funzione TAsyncCallsForm.AsyncMethod (taskNr, sleepTime: integer): intero;
inizio
risultato: = sleepTime;
Sonno (sleepTime);
TAsyncCalls.VCLInvoke (
procedura
inizio
Log (Formato ('done> nr:% d / task:% d / slept:% d', [tasknr, asyncHelper.TaskCount, sleepTime]));
fine);
fine;
TAsyncCalls.VCLInvoke è un modo per eseguire la sincronizzazione con il thread principale (il thread principale dell'applicazione - l'interfaccia utente dell'applicazione). VCLInvoke ritorna immediatamente. Il metodo anonimo verrà eseguito nel thread principale. C'è anche VCLSync che ritorna quando è stato chiamato il metodo anonimo nel thread principale.
Torna alla mia attività di "scansione dei file": quando si alimenta (in un ciclo for) il pool di thread asynccalls con una serie di chiamate TAsyncCalls.Invoke (), le attività verranno aggiunte all'interno del pool e verranno eseguite "quando arriva il momento" ( al termine delle chiamate aggiunte in precedenza).
La funzione AsyncMultiSync definita in asnyccalls attende il completamento delle chiamate asincrone (e di altri handle). Esistono alcuni modi sovraccarichi per chiamare AsyncMultiSync ed ecco il più semplice:
funzione AsyncMultiSync (const Elenco: matrice di IAsyncCall; WaitAll: Boolean = True; Millisecondi: Cardinale = INFINITO): Cardinale;
Se voglio avere "wait all" implementato, devo compilare un array di IAsyncCall ed eseguire AsyncMultiSync in sezioni di 61.
Ecco un pezzo del TAsyncCallsHelper:
ATTENZIONE: codice parziale! (codice completo disponibile per il download)
usi AsyncCalls;
genere
TIAsyncCallArray = matrice di IAsyncCall;
TIAsyncCallArrays = matrice di TIAsyncCallArray;
TAsyncCallsHelper = classe
privato
fTasks: TIAsyncCallArrays;
proprietà Attività: TIAsyncCallArrays leggere fTasks;
pubblico
procedura addTask (const chiama: IAsyncCall);
procedura WaitAll;
fine;
ATTENZIONE: codice parziale!
procedura TAsyncCallsHelper.WaitAll;
var
i: numero intero;
inizio
per i: = Alto (Attività) giù verso Basso (procedure) fare
inizio
AsyncCalls.AsyncMultiSync (procedure [i]);
fine;
fine;
In questo modo posso "aspettare tutto" in blocchi di 61 (MAXIMUM_ASYNC_WAIT_OBJECTS), ovvero in attesa di array di IAsyncCall.
Con quanto sopra, il mio codice principale per alimentare il pool di thread è simile a:
procedura TAsyncCallsForm.btnAddTasksClick (Mittente: TObject);
const
nrItems = 200;
var
i: numero intero;
inizio
asyncHelper.MaxThreads: = 2 * System.CPUCount;
Clearlog ( 'partire');
per i: = 1 a nrItems fare
inizio
asyncHelper.AddTask (TAsyncCalls.Invoke (AsyncMethod, i, Random (500)));
fine;
Log ('all in');
// aspetta tutto
//asyncHelper.WaitAll;
// o consenti l'annullamento di tutto non avviato facendo clic sul pulsante "Annulla tutto":
mentre NON asyncHelper.AllFinished fare Application.ProcessMessages;
Log ( 'finito');
fine;
Vorrei anche avere un modo di "annullare" quelle attività che si trovano nel pool ma che attendono la loro esecuzione.
Sfortunatamente, AsyncCalls.pas non fornisce un modo semplice per annullare un'attività una volta che è stata aggiunta al pool di thread. Non esiste IAsyncCall.Cancel o IAsyncCall.DontDoIfNotAlreadyExecuting o IAsyncCall.NeverMindMe.
Perché questo funzioni ho dovuto cambiare AsyncCalls.pas cercando di modificarlo il meno possibile - in modo che quando Andy rilascia una nuova versione devo solo aggiungere alcune righe per far funzionare la mia idea "Annulla attività".
Ecco cosa ho fatto: ho aggiunto una "procedura Annulla" a IAsyncCall. La procedura Annulla imposta il campo "FCancelled" (aggiunto) che viene verificato quando il pool sta per iniziare l'esecuzione dell'attività. Ho dovuto modificare leggermente IAsyncCall.Finished (in modo che i rapporti di una chiamata siano terminati anche se annullati) e la procedura TAsyncCall.InternExecuteAsyncCall (non eseguire la chiamata se è stata annullata).
Puoi usare WinMerge per individuare facilmente le differenze tra l'originale asynccall.pas di Andy e la mia versione modificata (inclusa nel download).
Puoi scaricare il codice sorgente completo ed esplorare.
Il CancelInvocation Il metodo interrompe la chiamata all'AsyncCall. Se AsyncCall è già stato elaborato, una chiamata a CancelInvocation non ha alcun effetto e la funzione Annullata restituisce False poiché AsyncCall non è stata annullata.
Il Annullato Il metodo restituisce True se AsyncCall è stato annullato da CancelInvocation.
Il Dimenticare Il metodo scollega l'interfaccia IAsyncCall dall'AsyncCall interno. Ciò significa che se l'ultimo riferimento all'interfaccia IAsyncCall scompare, la chiamata asincrona verrà comunque eseguita. I metodi dell'interfaccia genereranno un'eccezione se chiamati dopo aver chiamato Dimentica. La funzione asincrona non deve richiamare nel thread principale perché potrebbe essere eseguita dopo che il meccanismo TThread.Synchronize / Queue è stato chiuso da RTL, ciò che può causare un blocco morto.
Nota, tuttavia, che puoi comunque beneficiare del mio AsyncCallsHelper se devi aspettare che tutte le chiamate asincrone finiscano con "asyncHelper.WaitAll"; o se è necessario "Annulla tutto".