TNxHorizon
est entièrement thread-safeNXHorizon.Instance
, est thread-safe et peut être utilisée à partir de n'importe quel thread Déclarez le type d'événement :
Les événements sont classés par type d'informations - TypeInfo
. Chaque catégorie d'événement distincte nécessite un type distinct.
type
TFoo = class
...
end ;
TOtherFoo = type TFoo;
TIntegerEvent = type Integer;
TStringEvent = type string;
TFooEvent = INxEvent<TFoo>;
TOtherFooEvent = INxEvent<TOtherFoo>;
Inscription/désinscription à l'événement :
L'abonnement aux événements peut être ajouté à n'importe quelle classe existante.
type
TSubscriber = class
protected
// subscriptions
fIntegerSubscription: INxEventSubscription;
fStringSubscription: INxEventSubscription;
// event handlers
procedure OnIntegerEvent ( const aEvent: TIntegerEvent);
procedure OnStringEvent ( const aEvent: TStringEvent);
public
constructor Create;
destructor Destroy; override;
end ;
constructor TSubscriber.Create;
begin
fIntegerSubscription := NxHorizon.Instance.Subscribe<TIntegerEvent>(Async, OnIntegerEvent);
fStringSubscription := NxHorizon.Instance.Subscribe<TStringEvent>(Sync, OnStringEvent);
end ;
destructor TSubscriber.Destroy;
begin
fIntegerSubscription.WaitFor;
fStringSubscription.WaitFor;
NxHorizon.Instance.Unsubscribe(fIntegerSubscription);
NxHorizon.Instance.Unsubscribe(fStringSubscription);
inherited ;
end ;
procedure TSubscriber.OnIntegerEvent ( const aEvent: TIntegerEvent);
begin
Writeln(aEvent);
end ;
procedure TSubscriber.OnStringEvent ( const aEvent: TStringEvent);
begin
Writeln(aEvent);
end ;
Envoyer des messages :
NxHorizon.Instance.Post<TIntegerEvent>( 5 );
NxHorizon.Instance.Send<TStringEvent>( ' abc ' , Async);
ou
var
IntEvent: TIntegerEvent;
StrEvent: TStringEvent;
IntEvent := 5 ;
StrEvent := ' abc ' ;
NxHorizon.Instance.Post(IntEvent);
NxHorizon.Instance.Send(StrEvent, Async);
Les méthodes du gestionnaire d’événements doivent être conformes à la déclaration suivante, où T
peut être de n’importe quel type. La livraison asynchrone nécessite des types avec une gestion automatique de la mémoire ou des types de valeur. Vous pouvez également utiliser des instances d'objets de longue durée gérées manuellement comme événements, mais dans de tels cas, vous devez vous assurer qu'elles ne seront pas détruites avant que les messages déjà envoyés ne soient entièrement traités.
procedure( const aEvent: T) of object ;
Le type TNxHorizonDelivery
déclare quatre options de livraison :
Sync
- synchrone dans le thread actuelAsync
- asynchrone dans un thread d'arrière-plan aléatoireMainSync
- synchrone sur le thread principalMainAsync
- asynchrone sur le thread principal Sync
et MainSync
sont des opérations BLOCANTES, et le gestionnaire d'événements s'exécutera immédiatement dans le contexte du thread actuel, ou synchronisé avec le thread principal. Cela bloquera la distribution d'autres événements à l'aide de la même instance de bus d'événements jusqu'à ce que le gestionnaire d'événements ait terminé. Ne l'utilisez pas (ou utilisez-le avec parcimonie uniquement pour de courtes exécutions) sur l'instance de bus d'événements par défaut.
Si l'envoi d'événements est effectué à partir du contexte du thread principal, la livraison MainAsync
utilisera TThread.ForceQueue
pour exécuter le gestionnaire d'événements de manière asynchrone dans le contexte du thread principal.
L'abonnement à un gestionnaire d'événements construit une nouvelle instance INxEventSubscription
. Vous devez stocker l'instance renvoyée afin de vous désinscrire ultérieurement.
Il existe deux méthodes de désabonnement : Unsubscribe
et UnsubscribeAsync
.
Les deux méthodes annulent l’abonnement et le suppriment de la collection d’abonnements gérée dans le bus d’événements. Cette collection est itérée dans les méthodes Post
et Send
. Toute modification à ce moment-là n'est pas autorisée et pourrait entraîner un comportement inattendu.
Pour éviter la modification de la collection d'abonnés pendant l'itération, si vous souhaitez vous désabonner du code exécuté dans un gestionnaire d'événements distribué de manière synchrone, vous devez utiliser UnsubscribeAsync
, qui annulera immédiatement l'abonnement, mais retardera la suppression réelle de la collection, en l'exécutant en dehors du itération de répartition.
Les gestionnaires d'événements distribués de manière asynchrone s'exécutent toujours en dehors de l'itération de distribution et permettent d'utiliser la méthode Unsubscribe
. Cependant, la manière dont les gestionnaires sont distribués peut être modifiée par un code externe non lié, et si vous ne pouvez pas garantir de manière absolue une distribution asynchrone, l'utilisation de UnsubscribeAsync
est garantie.
Unsubscribe
et UnsubscribeAsync
annulent également l'abonnement, avant de le supprimer de la collection d'abonnements. Habituellement, il n'est pas nécessaire d'annuler explicitement l'abonnement avant de vous désinscrire, mais si vous avez une raison particulière pour laquelle vous souhaitez annuler l'abonnement à un moment donné avant de vous désinscrire, vous pouvez appeler sa méthode Cancel
. Cancel
peut être appelée plusieurs fois en toute sécurité. Une fois qu’un abonnement est annulé, son état ne peut plus être rétabli.
En raison de la répartition asynchrone des événements, il est possible qu'un gestionnaire d'événements soit déjà distribué au moment où vous annulez ou désabonnez un abonnement particulier. Si vous vous désabonnez d'un destructeur, votre destructeur de classe d'abonné, cela pourrait vous amener à accéder à l'instance d'abonné pendant son processus de destruction ou après sa destruction. Pour éviter un tel scénario, vous pouvez appeler WaitFor
sur l'abonnement, ce qui annulera immédiatement l'abonnement et le bloquera jusqu'à ce que tous les gestionnaires d'événements envoyés aient fini de s'exécuter.
Si vous appelez WaitFor
à partir du contexte du thread principal et que vos gestionnaires d'événements s'exécutent pendant une longue période, votre application cessera de répondre pendant cette période.
Les méthodes BeginWork
et EndWork
font partie du mécanisme d'attente d'abonnement. Si vous devez exécuter du code dans un gestionnaire d'événements dans un autre thread et que vous devez vous assurer que ce code sera également attendu, vous pouvez appeler BeginWork
avant de démarrer un tel thread et EndWork
une fois qu'il est terminé. Assurez-vous que tous les chemins de code finiront par appeler un EndWork
correspondant, car ne pas le faire entraînera un blocage lorsque vous appellerez WaitFor
.
procedure TSubscriber.OnLongEvent ( const aEvent: TIntegerEvent);
begin
fIntegerSubscription.BeginWork;
try
TTask.Run(
procedure
begin
try
...
finally
fIntegerSubscription.EndWork;
end ;
end );
except
fIntegerSubscription.EndWork;
raise;
end ;
end ;
procedure Post <T>( const aEvent: T);
procedure Send <T>( const aEvent: T; aDelivery: TNxHorizonDelivery);
La méthode Post
est utilisée pour publier des événements où l'option de livraison dépendra de l'option de livraison de l'abonnement définie lors de l'abonnement à l'événement.
La méthode Send
remplace l'option de remise de l'abonnement et distribue un événement d'une manière déterminée par le paramètre aDelivery
transmis. Si l'abonnement spécifie la répartition dans le contexte du thread principal, la méthode Send
respectera cette exigence, vous n'aurez donc pas à vous soucier de la synchronisation dans ces gestionnaires d'événements.
Le fait que Post
ou Send
bloque les appels dépend des options de livraison utilisées. Lorsque vous utilisez Post
, veuillez noter que différents abonnements au même type d'événement peuvent être configurés avec différentes options de livraison.
TNxHorizon
est une classe gérée manuellement et entièrement thread-safe. Vous pouvez créer autant d'instances de bus d'événements distinctes que vous le souhaitez. Les instances sont entièrement thread-safe et ne nécessitent aucune protection supplémentaire tant que vous utilisez des références en mode lecture seule. Une fois que vous avez initialisé la référence et commencé à utiliser cette instance dans plusieurs threads, vous n'êtes pas autorisé à modifier la variable de référence elle-même. . Vous pouvez librement appeler n'importe quelle méthode sur une telle référence à partir de n'importe quel thread.
Si vous devez prendre en charge différents canaux (catégorisation d'événements supplémentaire), vous pouvez obtenir cette fonctionnalité en créant une instance de bus d'événements distincte pour chaque canal.
La fonctionnalité de la classe TNxHorizon
ne peut pas être directement exposée en tant qu'interface car elle utilise des méthodes paramétrées qui ne sont pas prises en charge pour les interfaces.
Outre l'instance singleton disponible via NxHorizon.Instance
il est possible d'utiliser des instances de bus distinctes à d'autres fins, avec une durée de vie beaucoup plus courte. Afin de simplifier la gestion de la durée de vie de ces instances et d'éviter d'accéder à des pointeurs en suspens dans un environnement multithread, vous pouvez utiliser INxHorizon
pour conserver et partager en toute sécurité de telles instances de bus d'événements.
Cela ouvre également la possibilité d'utiliser des instances de bus d'événements, qui sont plutôt légères en tant que mécanisme de répartition dans le modèle d'observateur , où le sujet observable détient et expose sa référence INxHorizon
, à laquelle les observateurs peuvent s'attacher. Lors de l'abonnement, les observateurs doivent stocker l'instance INxHorizon
à laquelle ils sont abonnés, afin de pouvoir s'en désabonner en toute sécurité, même si le sujet lui-même a été publié entre-temps.
Cela permet d'utiliser le modèle d'observateur de manière thread-safe avec des sujets qui ne sont pas des instances automatiquement gérées. De plus, le fait de conserver une référence forte (thread-safe) à l'instance de bus d'événements au lieu du sujet évite directement les cycles de référence potentiels lors de l'utilisation d'instances d'objet géré, au lieu d'utiliser des références faibles non sécurisées pour les threads.
INxHorizon.Instance
renvoie une instance TNxHorizon
encapsulée qui est gérée manuellement par un conteneur. Il peut être utilisé en toute sécurité tant que l’abonné fait fortement référence à son conteneur.
Le sujet doit appeler la méthode ShutDown
sur sa référence INxHorizon
lors de son processus de nettoyage. Cela définira l'indicateur IsActive
sur False
et enverra TNxHorizonShutDownEvent
à ses abonnés, afin qu'ils puissent effectuer un nettoyage approprié. TNxHorizonShutDownEvent
contient une instance TNxHorizon
encapsulée, afin que les abonnés puissent utiliser un seul gestionnaire d'événements d'arrêt pour gérer plusieurs sujets.
L’appel ShutDown
n’a aucun impact sur la capacité du bus à envoyer et publier des messages. Si vous devez vous assurer que vous ne distribuez pas de nouveaux événements pendant le processus de nettoyage, vous pouvez vérifier l'indicateur IsActive
avant d'appeler Post
ou Send
.
Ce bus d'événements utilise TTask
du PPL pour la répartition asynchrone des événements dans XE7 et les versions Delphi plus récentes. Ces tâches s'exécutent sur le pool de threads par défaut. C'est par conception. Ceci est basé sur le principe selon lequel tout code utilisant le pool de threads par défaut doit s'exécuter très rapidement et ne doit pas provoquer de conflits.
Si vous avez du code dans les gestionnaires d'événements ou un autre code qui utilise le pool par défaut pour des tâches de longue durée susceptibles de causer des problèmes, la bonne marche à suivre consiste à exécuter ce code spécifique de longue durée sur un pool de threads distinct et dédié. de créer plusieurs pools de threads partout qui serviront différentes parties des frameworks qui doivent exécuter certaines tâches.
Pour un gestionnaire d'événements de longue durée, la solution la plus rapide au problème consiste à utiliser la répartition synchrone et à démarrer une nouvelle tâche dans le code du gestionnaire d'événements qui peut ensuite utiliser un autre pool de threads non par défaut. De cette façon, vous aurez plus de contrôle sur votre code et la liberté de modifier le comportement d'un gestionnaire spécifique sans affecter tous les autres gestionnaires exécutés sur la même instance de bus d'événements :
procedure TSubscriber.OnLongEvent ( const aEvent: TLongEvent);
begin
TTask.Run(
procedure
begin
...
end , DedicatedThreadPool);
end ;
Les principales caractéristiques de cette implémentation de bus d'événements sont la sécurité des threads, la vitesse et la simplicité. Les fonctionnalités et extensions supplémentaires ne doivent pas compromettre ces objectifs et intentions d’origine.
Cette implémentation est également basée sur mes propres exigences et mon code, et il est possible que certaines parties ne satisfassent pas pleinement à un autre flux de travail de code courant.
Étant donné que la vitesse est basée sur la mise en œuvre actuelle des méthodes Post
et Send
, je ne m'attends pas à beaucoup de changements dans ces domaines. Cependant, il est possible d'améliorer ou de prendre en charge différents flux de travail d'abonnement en dehors de ces deux méthodes.
https://dalija.prasnikar.info