Exemple de pool de threads Delphi utilisant AsyncCalls

Ceci est mon prochain projet de test pour voir quelle bibliothèque de threads pour Delphi me conviendrait le mieux pour ma tâche "d'analyse de fichiers" que je voudrais traiter dans plusieurs threads / dans un pool de threads.

Pour répéter mon objectif: transformer mon "analyse de fichiers" séquentielle de 500-2000 + fichiers de l'approche non threadée en une approche threadée. Je ne devrais pas avoir 500 threads en cours d'exécution en même temps, je voudrais donc utiliser un pool de threads. Un pool de threads est une classe de type file d'attente alimentant un certain nombre de threads en cours d'exécution avec la tâche suivante de la file d'attente.

La première tentative (très basique) a été faite en étendant simplement la classe TThread et en implémentant la méthode Execute (mon analyseur de chaîne threadé).

Étant donné que Delphi n'a pas de classe de pool de threads implémentée hors de la boîte, dans ma deuxième tentative, j'ai essayé d'utiliser OmniThreadLibrary par Primoz Gabrijelcic.

OTL est fantastique, a des millions de façons d'exécuter une tâche en arrière-plan, un chemin à parcourir si vous voulez avoir une approche "feu et oubliez" pour gérer l'exécution filetée de morceaux de votre code.

AsyncCalls par Andreas Hausladen

Remarque: ce qui suit serait plus facile à suivre si vous téléchargez d'abord le code source.

En explorant d'autres façons d'exécuter certaines de mes fonctions de manière filetée, j'ai décidé d'essayer également l'unité "AsyncCalls.pas" développée par Andreas Hausladen. Andy AsyncCalls - L'unité d'appels de fonctions asynchrones est une autre bibliothèque qu'un développeur Delphi peut utiliser pour soulager la peine d'implémenter une approche filetée pour exécuter du code.

Du blog d'Andy: Avec AsyncCalls, vous pouvez exécuter plusieurs fonctions en même temps et les synchroniser à chaque point de la fonction ou de la méthode qui les a démarrées… L'unité AsyncCalls propose une variété de prototypes de fonctions pour appeler des fonctions asynchrones… Elle implémente un pool de threads! L'installation est super facile: utilisez simplement des appels asynchrones depuis n'importe laquelle de vos unités et vous avez un accès instantané à des choses comme "exécuter dans un thread séparé, synchroniser l'interface utilisateur principale, attendre la fin".

Outre les AsyncCalls gratuits à utiliser (licence MPL), Andy publie également fréquemment ses propres correctifs pour l'IDE Delphi comme "Delphi Speed ​​Up" et "DDevExtensions", je suis sûr que vous en avez entendu parler (si vous ne l'utilisez pas déjà).

AsyncCalls en action

En substance, toutes les fonctions AsyncCall renvoient une interface IAsyncCall qui permet de synchroniser les fonctions. IAsnycCall expose les méthodes suivantes:

     
     
     
     
 //v 2.98 de asynccalls.pas
IAsyncCall = interface
// attend que la fonction soit terminée et retourne la valeur de retour
fonction Sync: Entier;
// renvoie True lorsque la fonction asynchrone est terminée
fonction Terminé: booléen;
// renvoie la valeur de retour de la fonction asynchrone, lorsque Finished est TRUE
function ReturnValue: Integer;
// indique à AsyncCalls que la fonction affectée ne doit pas être exécutée dans le threa actuel
procedure ForceDifferentThread;
fin;

Voici un exemple d'appel à une méthode qui attend deux paramètres entiers (renvoyant un IAsyncCall):

     
     
     
     
 TAsyncCalls.Invoke (AsyncMethod, i, Random (500));
     
     
     
     
une fonction TAsyncCallsForm.AsyncMethod (taskNr, sleepTime: integer): entier;
commencer
résultat: = sleepTime;
Sleep (sleepTime);
TAsyncCalls.VCLInvoke (
procédure
commencer
Log (Format ('done> nr:% d / tasks:% d / sleeped:% d', [tasknr, asyncHelper.TaskCount, sleepTime]));
fin);
fin;

TAsyncCalls.VCLInvoke est un moyen de faire la synchronisation avec votre thread principal (thread principal de l'application - l'interface utilisateur de votre application). VCLInvoke revient immédiatement. La méthode anonyme sera exécutée dans le thread principal. Il y a aussi VCLSync qui retourne lorsque la méthode anonyme a été appelée dans le thread principal.

Pool de threads dans AsyncCalls

Revenons à ma tâche "d'analyse de fichiers": lors de l'alimentation (dans une boucle for) du pool de threads asynccalls avec une série d'appels TAsyncCalls.Invoke (), les tâches seront ajoutées au pool interne et seront exécutées "le moment venu" ( lorsque les appels ajoutés précédemment sont terminés).

Attendez que tous les appels IAsync se terminent

La fonction AsyncMultiSync définie dans asnyccalls attend la fin des appels asynchrones (et des autres descripteurs). Il existe plusieurs façons surchargées d'appeler AsyncMultiSync, et voici la plus simple:

     
     
     
     
une fonction AsyncMultiSync (const Liste: tableau de IAsyncCall; WaitAll: Boolean = True; Millisecondes: Cardinal = INFINI): Cardinal;

Si je veux avoir "wait all" implémenté, je dois remplir un tableau d'IAsyncCall et faire AsyncMultiSync en tranches de 61.

Mon assistant AsnycCalls

Voici un morceau de TAsyncCallsHelper:

     
     
     
     
ATTENTION: code partiel! (code complet disponible pour téléchargement)
les usages AsyncCalls;
type
TIAsyncCallArray = tableau de IAsyncCall;
TIAsyncCallArrays = tableau de TIAsyncCallArray;
TAsyncCallsHelper = classe
privé
fTasks: TIAsyncCallArrays;
propriété Tâches: TIAsyncCallArrays lis fTasks;
Publique
procédure Ajouter une tâche(const appel: IAsyncCall);
procédure WaitAll;
fin;
     
     
     
     
ATTENTION: code partiel!
procédure TAsyncCallsHelper.WaitAll;
var
i: entier;
commencer
pour i: = élevé (tâches) vers le bas Faible (tâches) faire
commencer
AsyncCalls.AsyncMultiSync (tâches [i]);
fin;
fin;

De cette façon, je peux "attendre tous" en morceaux de 61 (MAXIMUM_ASYNC_WAIT_OBJECTS) - c'est-à-dire attendre les tableaux de IAsyncCall.

Avec ce qui précède, mon code principal pour alimenter le pool de threads ressemble à:

     
     
     
     
procédure TAsyncCallsForm.btnAddTasksClick (Sender: TObject);
const
nrItems = 200;
var
i: entier;
commencer
asyncHelper.MaxThreads: = 2 * System.CPUCount;
ClearLog ('démarrage');
pour i: = 1 à nrItems faire
commencer
asyncHelper.AddTask (TAsyncCalls.Invoke (AsyncMethod, i, Random (500)));
fin;
Journal («tout en»);
// attend tout
//asyncHelper.WaitAll;
// ou autorisez l'annulation de tout ce qui n'a pas commencé en cliquant sur le bouton "Annuler tout":

sans être asyncHelper.AllFinished faire Application.ProcessMessages;
Journal («terminé»);
fin;

Annuler tout? - Faut changer les AsyncCalls.pas :(

J'aimerais également avoir un moyen "d'annuler" les tâches qui sont dans le pool mais qui attendent leur exécution.

Malheureusement, AsyncCalls.pas ne fournit pas un moyen simple d'annuler une tâche une fois qu'elle a été ajoutée au pool de threads. Il n'y a pas IAsyncCall.Cancel ou IAsyncCall.DontDoIfNotAlreadyExecuting ou IAsyncCall.NeverMindMe.

Pour que cela fonctionne, j'ai dû modifier AsyncCalls.pas en essayant de le modifier le moins possible - de sorte que lorsque Andy publie une nouvelle version, je n'ai qu'à ajouter quelques lignes pour que mon idée "Annuler la tâche" fonctionne.

Voici ce que j'ai fait: j'ai ajouté une "procédure Annuler" à l'IAsyncCall. La procédure Annuler définit le champ "FCancelled" (ajouté) qui est vérifié lorsque le pool est sur le point de commencer l'exécution de la tâche. J'avais besoin de modifier légèrement l'IAsyncCall.Finished (pour qu'un rapport d'appel se termine même en cas d'annulation) et la procédure TAsyncCall.InternExecuteAsyncCall (pour ne pas exécuter l'appel s'il a été annulé).

Vous pouvez utiliser WinMerge pour localiser facilement les différences entre le asynccall.pas d'origine d'Andy et ma version modifiée (incluse dans le téléchargement).

Vous pouvez télécharger le code source complet et explorer.

Confession

REMARQUER! :)

     
     
     
     
le AnnulerInvocation La méthode empêche l'AsyncCall d'être invoquée. Si AsyncCall est déjà traité, un appel à CancelInvocation n'a aucun effet et la fonction Canceled renverra False car AsyncCall n'a pas été annulé..
le Annulé renvoie True si AsyncCall a été annulée par CancelInvocation.
le Oublier dissocie l'interface IAsyncCall de l'AsyncCall interne. Cela signifie que si la dernière référence à l'interface IAsyncCall a disparu, l'appel asynchrone sera toujours exécuté. Les méthodes de l'interface lèveront une exception si elles sont appelées après avoir appelé Forget. La fonction asynchrone ne doit pas appeler dans le thread principal car elle pourrait être exécutée après l'arrêt du mécanisme TThread.Synchronize / Queue par la RTL, ce qui peut provoquer un verrou mort..

Notez, cependant, que vous pouvez toujours bénéficier de mon AsyncCallsHelper si vous devez attendre que tous les appels asynchrones se terminent par "asyncHelper.WaitAll"; ou si vous devez "Annuler tout".