Quand une interrogation pour des événements serait-elle préférable à un motif d'observateur?

41

Existe-t-il des scénarios dans lesquels interroger des événements serait préférable à l'utilisation du modèle d'observateur ? J'ai peur d'utiliser les sondages et je ne commencerais à les utiliser que si quelqu'un me donnait un bon scénario. Tout ce à quoi je peux penser, c'est comment le modèle d'observateur est meilleur que le sondage. Considérez ce scénario:

Vous programmez un simulateur de voiture. La voiture est un objet. Dès que la voiture s'allume, vous voulez jouer un extrait sonore "vroom vroom".

Vous pouvez modéliser ceci de deux manières:

Polling : interroge l'objet car toutes les secondes pour voir s'il est activé. Quand il est allumé, jouez le clip audio.

Modèle d'observateur : Faites de la voiture le sujet du modèle d'observateur. Faites-le publier l'événement "on" à tous les observateurs quand il s'allume. Créez un nouvel objet sonore qui écoute la voiture. Faites-le implémenter le rappel "on", qui lit le clip audio.

Dans ce cas, je pense que le motif observateur gagne. Premièrement, l'interrogation utilise davantage de processeur. Deuxièmement, le clip sonore ne se déclenche pas immédiatement lorsque la voiture s'allume. Il peut y avoir un intervalle pouvant aller jusqu'à 1 seconde en raison de la période de scrutin.

JoJo
la source
Je peux penser à presque aucun scénario. Le modèle d'observateur est ce qui correspond réellement au monde réel et à la vie réelle. Ainsi, je pense qu'aucun scénario ne justifierait jamais de ne pas l'utiliser.
Saeed Neamati
Parlez-vous d' événements d' interface utilisateur ou d'événements en général?
Bryan Oakley
3
Votre exemple ne supprime pas le problème d'interrogation / observation. Vous l'avez simplement passé à un niveau inférieur. Votre programme doit encore déterminer si la voiture est allumée ou non par un mécanisme quelconque.
Dunk

Réponses:

55

Imaginez que vous souhaitiez être averti à chaque cycle du moteur, par exemple pour afficher une mesure du régime du conducteur.

Modèle d'observateur: le moteur publie un événement "cycle moteur" à tous les observateurs pour chaque cycle. Créez un écouteur qui compte les événements et met à jour l'affichage RPM.

Interrogation: l' affichage du régime demande au moteur un compteur de cycles à intervalles réguliers et met à jour l'affichage du régime en conséquence.

Dans ce cas, le modèle d'observateur serait probablement perdu: le cycle du moteur est un processus à haute fréquence et haute priorité, vous ne voulez pas retarder ou bloquer ce processus juste pour mettre à jour un affichage. Vous ne souhaitez pas non plus écraser le pool de threads avec des événements de cycle de moteur.


PS: J'utilise aussi fréquemment le mode d'interrogation dans la programmation distribuée:

Modèle d'observateur: le processus A envoie au processus B un message indiquant "chaque fois qu'un événement E se produit, envoie un message au processus A".

Mode d’ interrogation : Le processus A envoie régulièrement au processus B un message indiquant "Si votre événement E s’est produit depuis la dernière interrogation, envoyez-moi un message maintenant".

Le modèle d'interrogation produit un peu plus de charge réseau. Mais le modèle d'observateur a aussi des inconvénients:

  • Si le processus A se bloque, il ne se désabonnera jamais et le processus B tentera de lui envoyer des notifications pour toute l'éternité, à moins qu'il ne puisse détecter de manière fiable les échecs de processus distants (chose difficile à faire).
  • Si l'événement E est très fréquent et / ou si les notifications contiennent beaucoup de données, le processus A peut recevoir plus de notifications d'événements qu'il ne peut en gérer. Avec le mode d'interrogation, il peut simplement limiter l'interrogation.
  • Dans le mode observateur, une charge élevée peut provoquer des "ondulations" dans tout le système. Si vous utilisez des prises de blocage, ces ondulations peuvent aller dans les deux sens.
Nikie
la source
1
Bon point. Parfois, il vaut mieux interroger pour des raisons de performance.
Falcon
1
Le nombre prévu d'observateurs est également à prendre en compte. Lorsque vous attendez un grand nombre d'observateurs, leur mise à jour à partir de l'observé peut devenir un goulot d'étranglement. Il est beaucoup plus facile d'écrire une valeur quelque part et de demander aux "observateurs" de vérifier cette valeur lorsqu'ils en ont besoin.
Marjan Venema
1
"à moins qu'il ne puisse détecter de manière fiable les échecs de processus distants (chose difficile à faire)" ... sauf par interrogation; P. Ainsi, la meilleure conception consiste à minimiser autant que possible la réponse «rien n’a changé». +1, bonne réponse.
pdr
2
@ Jojo: Vous pouvez, oui, mais alors vous mettez la politique qui devrait appartenir à l'affichage dans le compteur RPM. Peut-être que l'utilisateur souhaite parfois avoir un affichage très précis de la vitesse de rotation.
Zan Lynx
2
@JoJo: La publication de chaque 100ème événement est un hack. Cela ne fonctionne bien que si la fréquence des événements est toujours dans la bonne plage, si le traitement de l'événement ne prend pas trop de temps pour le moteur, si tous les abonnés ont besoin d'une précision comparable. Et cela prend une opération de module par tour / minute, ce qui représente (en supposant quelques milliers de tours par minute) beaucoup plus de travail pour le processeur que quelques opérations d'interrogation par seconde.
Nikie
7

La scrutation est préférable si le processus de scrutation est beaucoup plus lent que les choses qu'il scrute. Si vous écrivez des événements dans une base de données, il est souvent préférable d'interroger tous vos producteurs d'événements, de collecter tous les événements survenus depuis le dernier interrogation, puis de les écrire en une seule transaction. Si vous tentiez d'écrire chaque événement au fur et à mesure qu'il se produisait, vous ne pourriez peut-être pas suivre le rythme et finiriez par avoir des problèmes lorsque vos files d'attente de saisie s'épuisent. Cela a également plus de sens dans les systèmes distribués à couplage lâche, où la latence est élevée ou où l'établissement et le démontage de la connexion sont coûteux. Je trouve les systèmes de sondage plus faciles à écrire et à comprendre, mais dans la plupart des situations, les observateurs ou les consommateurs événementiels semblent offrir de meilleures performances (selon mon expérience).

RGT
la source
7

Il est beaucoup plus facile d' interroger un réseau lorsque les connexions peuvent échouer, les serveurs peuvent être utilisés, etc. Rappelez-vous qu'à la fin de la journée, un socket TCP doit «interroger» les messages Keep-a-Live, sinon le serveur assumera le contrôle du client. est parti.

L'interrogation est également utile lorsque vous souhaitez conserver une interface utilisateur mise à jour, mais les objets sous-jacents évoluant très rapidement , il est inutile de mettre à jour l'interface utilisateur plus de quelques fois par seconde dans la plupart des applications.

Si le serveur peut répondre «pas de changement» à un coût très bas, si vous n'interrogez pas trop souvent et que vous n'avez pas 1000 interrogations, l'interrogation fonctionne très bien dans la vie réelle.

Cependant, pour les cas « en mémoire », j’utilise par défaut le motif observateur car c’est normalement le moins de travail.

Ian
la source
5

Les sondages ont des inconvénients, vous les avez déjà énoncés dans votre question.

Cependant, cela peut être une meilleure solution lorsque vous voulez vraiment dissocier l'observable de l'observateur. Mais parfois, il peut être préférable d’utiliser un emballage observable pour que l’objet soit observé dans de tels cas.

Je n'utilisais l'interrogation que lorsque l'observable ne pouvait pas être observé avec les interactions d'objet, ce qui est souvent le cas lors de l'interrogation de bases de données, par exemple, pour lesquelles il est impossible d'avoir des rappels. Un autre problème peut être le multithreading, où il est souvent plus sûr d'interroger et de traiter les messages plutôt que d'appeler directement des objets, afin d'éviter les problèmes de simultanéité.

Faucon
la source
Je ne suis pas tout à fait sûr de savoir pourquoi vous croyez que les sondages sont plus sûrs pour le multi-threading. Dans la plupart des cas, ce ne sera pas le cas. Lorsque le gestionnaire d'interrogation reçoit une demande d'interrogation, il devait déterminer l'état de l'objet interrogé. Si l'objet est en cours de mise à jour, il n'est pas sûr pour le gestionnaire d'interrogation. Dans le scénario d'écoute, vous recevez des notifications uniquement si le pousseur est dans un état cohérent. Vous pouvez ainsi éviter la plupart des problèmes de synchronisation dans l'objet interrogé.
Lie Ryan
4

Pour un bon exemple de ce qui se passe lorsque l'interrogation prend le relais de la notification, examinez les piles de réseaux du système d'exploitation.

C'était un gros problème pour Linux lorsque la pile de mise en réseau a activé NAPI, une API de mise en réseau permettant aux pilotes de passer d'un mode d'interruption (notification) à un mode d'interrogation.

Avec plusieurs interfaces Ethernet gigabit, les interruptions surchargeraient souvent le processeur, ce qui ralentirait le système. Lors de l'interrogation, les cartes réseau collectent les paquets dans des mémoires tampons jusqu'à leur interrogation ou les cartes écrivent même les paquets en mémoire via DMA. Ensuite, lorsque le système d'exploitation est prêt, il interroge la carte pour toutes ses données et effectue le traitement TCP / IP standard.

Le mode d'interrogation permet à la CPU de collecter des données Ethernet à son taux de traitement maximum sans charge d'interruption inutile. Le mode d'interruption permet à la CPU de rester inactive entre les paquets lorsque le travail n'est pas trop occupé.

Le secret est de savoir quand passer d’un mode à l’autre. Chaque mode présente des avantages et doit être utilisé au bon endroit.

Zan Lynx
la source
2

J'adore voter! Est ce que je? Oui! Est ce que je? Oui! Est ce que je? Oui! Est-ce que je reste? Oui! Et maintenant? Oui!

Comme d'autres l'ont mentionné, il peut s'avérer extrêmement inefficace de procéder à une interrogation uniquement pour retrouver le même état inchangé à maintes reprises. Telle est la recette pour brûler les cycles du processeur et réduire considérablement la durée de vie de la batterie des appareils mobiles. Bien sûr, il n’est pas inutile de retrouver à chaque fois un état nouveau et significatif à un rythme pas plus rapide que souhaité.

Mais la raison principale pour laquelle j'aime les sondages est sa simplicité et sa nature prévisible. Vous pouvez suivre le code et voir facilement quand et où les choses vont se passer et dans quel fil. Si, en théorie, nous vivions dans un monde où les sondages constituaient un gaspillage négligeable (bien que la réalité soit loin de là), alors je pense que cela simplifierait considérablement le maintien du code. Et c’est l’avantage de procéder à des sondages et à des levées car je vois si nous pourrions faire abstraction des performances, même si nous ne le devrions pas dans ce cas.

Quand j'ai commencé à programmer à l'époque du DOS, mes petits jeux tournaient autour du sondage. J'ai copié du code d'assemblage d'un livre que je comprenais à peine en ce qui concerne les interruptions de clavier et je lui ai fait stocker un tampon d'états de clavier, point auquel ma boucle principale était toujours en scrutation. La touche haut est-elle enfoncée? Nan. La touche haut est-elle enfoncée? Nan. Et maintenant? Nan. À présent? Oui. Ok, déplace le joueur.

Et bien qu’incroyablement inutile, j’ai trouvé qu’il était tellement plus facile de raisonner par rapport à ces jours de programmation multitâche et événementielle. Je savais exactement quand et où les choses se produiraient à tout moment et il était plus facile de garder des taux de trame stables et prévisibles sans problème.

Ainsi, depuis lors, j'ai toujours essayé de trouver un moyen de tirer parti des avantages et de la prévisibilité de cette opération sans brûler réellement les cycles du processeur, par exemple en utilisant des variables de condition pour informer les threads de se réveiller à quel moment ils peuvent extraire le nouvel état, faire leur truc, et se rendormir en attendant d'être averti à nouveau.

Et d’une manière ou d’une autre, je trouve que les files d’événements sont beaucoup plus faciles à utiliser, du moins que les modèles d’observateurs, même s’ils ne facilitent pas encore autant la tâche de prédire où vous allez finir ou ce qui va se passer. Ils centralisent au moins le flux de contrôle de gestion des événements sur quelques zones clés du système et traitent toujours ces événements dans le même thread au lieu de rebondir d'une fonction à un endroit complètement distant et inattendu tout à coup en dehors d'un thread de gestion d'événements central. Donc, la dichotomie ne doit pas toujours être entre observateurs et sondages. Les files d’événements sont en quelque sorte un terrain d’entente.

Mais oui, d’une manière ou d’une autre, j’ai tellement de facilité à raisonner au sujet de systèmes qui font des choses qui sont analogiquement plus proches du type de flux de contrôle prévisibles que j’avais quand j’interrogeais il ya très longtemps, tout en contrant la tendance du travail à se produire moments où aucun changement d'état n'a eu lieu. Il y a donc cet avantage si vous pouvez le faire d'une manière qui ne brûle pas inutilement les cycles du processeur, contrairement aux variables de condition.

Boucles homogènes

D'accord, j'ai eu un excellent commentaire Josh Caswellqui a mis en évidence certaines maladresses dans ma réponse:

"comme utiliser des variables de condition pour avertir les threads de se réveiller" Sonne comme un arrangement basé sur les événements / observateur

Techniquement, la variable condition elle-même applique le motif de l'observateur pour réveiller / notifier les threads. Par conséquent, appeler cette "interrogation" serait probablement extrêmement trompeur. Mais je trouve que cela procure un avantage similaire à celui obtenu lors de l'interrogation à partir des jours DOS (juste en termes de flux de contrôle et de prévisibilité). Je vais essayer de l'expliquer mieux.

Ce que j’ai trouvé attrayant à l’époque, c’était que vous pouviez consulter une section de code ou y faire un suivi et dire: «Très bien, toute cette section est dédiée à la gestion des événements de clavier. Rien d’autre ne se passera dans cette section de code. Et je sais exactement ce qui va se passer avant, et je sais exactement ce qui va se passer après (physique et rendu, par exemple). " L'interrogation des états du clavier vous a donné ce type de centralisation du flux de contrôle en ce qui concerne le traitement de ce qui devrait se passer en réponse à cet événement externe. Nous n'avons pas réagi à cet événement externe immédiatement. Nous avons répondu à notre convenance.

Lorsque nous utilisons un système Push basé sur un modèle Observer, nous perdons souvent ces avantages. Un contrôle peut être redimensionné, ce qui déclenche un événement de redimensionnement. Lorsque nous traçons à travers, nous nous trouvons dans un contrôle exotique qui effectue beaucoup de choses personnalisées lors de son redimensionnement, ce qui déclenche plus d'événements. Nous finissons par être complètement surpris de retrouver tous ces événements en cascade pour savoir où nous nous retrouvons dans le système. En outre, nous pourrions constater que tout cela ne se produit même pas systématiquement dans un thread donné, car le thread A pourrait redimensionner un contrôle ici alors que le thread B redimensionnait également un contrôle ultérieurement. J'ai donc toujours trouvé cela très difficile à raisonner étant donné combien il est difficile de prédire où tout se passe et ce qui va se passer.

La file d’événements est pour moi un peu plus simple à raisonner car elle simplifie l’emplacement de toutes ces choses, au moins au niveau des threads. Cependant, beaucoup de choses disparates pourraient se produire. Une file d'attente d'événements peut contenir un mélange éclectique d'événements à traiter, et chacun d'entre eux peut encore nous surprendre quant à la cascade d'événements survenus, à l'ordre dans lequel ils ont été traités et à la manière dont nous finissons par rebondir dans la base de code. .

Ce que je considère comme "proche" de l'interrogation ne devrait pas utiliser une file d'attente d'événements mais différer un type de traitement très homogène. A l' PaintSystemaide d'une variable de condition, il peut être alerté que des travaux de peinture sont nécessaires pour repeindre certaines cellules de la grille d'une fenêtre. À ce stade, il effectue une simple boucle séquentielle entre les cellules et repeint tout ce qui s'y trouve dans le bon ordre de z. Il se peut qu’il existe un niveau d’appel indirection / dispatch dynamique pour déclencher les événements de peinture de chaque widget résidant dans une cellule à repeindre, mais c’est tout - une couche d’appels indirects. La variable condition utilise le modèle observateur pour alerter PaintSystemqu’elle a du travail à faire, mais elle ne spécifie rien de plus que cela, et le paramètrePaintSystemest consacré à une tâche uniforme et très homogène à ce stade. Lorsque nous déboguons et suivons dans le PaintSystem'scode, nous savons qu’il ne se passera rien d’autre que de peindre.

Il s’agit donc essentiellement de faire en sorte que ces systèmes effectuent des boucles homogènes sur des données en appliquant une responsabilité très singulière, au lieu de boucles non homogènes sur des types de données disparates et en assumant de nombreuses responsabilités, comme nous pourrions l’obtenir avec le traitement des files d’événements.

Nous visons ce genre de chose:

when there's work to do:
   for each thing:
       apply a very specific and uniform operation to the thing

Par opposition à:

when one specific event happens:
    do something with relevant thing
in relevant thing's event:
    do some more things
in thing1's triggered by thing's event:
    do some more things
in thing2's event triggerd by thing's event:
    do some more things:
in thing3's event triggered by thing2's event:
    do some more things
in thing4's event triggered by thing1's event:
    cause a side effect which shouldn't be happening
    in this order or from this thread.

Et ainsi de suite. Et il ne doit pas nécessairement y avoir un fil par tâche. Un thread peut appliquer une logique de mise en page (redimensionnement / repositionnement) aux contrôles de l'interface graphique et les repeindre, mais il peut ne pas gérer les clics du clavier ou de la souris. Vous pouvez donc considérer cela comme une simple amélioration de l'homogénéité d'une file d'attente d'événements. Mais nous n’avons pas non plus besoin d’utiliser une file d’événements et d’entrelacer les fonctions de redimensionnement et de dessin. Nous pouvons faire comme:

in thread dedicated to layout and painting:
    when there's work to do:
         for each widget that needs resizing/reposition:
              resize/reposition thing to target size/position
              mark appropriate grid cells as needing repainting
         for each grid cell that needs repainting:
              repaint cell
         go back to sleep

Ainsi, l'approche ci-dessus utilise simplement une variable de condition pour informer le fil quand il y a du travail à faire, mais elle n'entrelace pas différents types d'événements (redimensionner dans une boucle, peindre dans une autre boucle, pas un mélange des deux) et cela ne se produit pas. pas la peine de communiquer quel est exactement le travail à faire (le fil de discussion "découvre" qu’au réveil, en regardant les états du système à l’échelle du système). Chaque boucle qu’elle effectue est alors de nature très homogène, ce qui permet de raisonner facilement sur l’ordre dans lequel tout se passe.

Je ne sais pas comment appeler ce type d'approche. Je n'ai pas vu d'autres moteurs d'interface graphique faire cela et c'est un peu ma propre approche exotique à la mienne. Mais avant d’essayer d’implémenter des frameworks d’interface graphique multithread à l’aide d’observateurs ou de files d’événements, j’avais énormément de difficulté à le déboguer et j’ai rencontré des conditions de course obscures et des blocages que je n’étais pas assez malin pour régler de manière à me mettre en confiance. à propos de la solution (certaines personnes pourraient le faire mais je ne suis pas assez intelligent). Ma conception de la première itération consistait simplement à appeler une fente directement par le biais d'un signal. Certaines fentes généraient alors d'autres threads pour effectuer un travail asynchrone. C'était le plus difficile à raisonner et je trébuchais sur les conditions de course et les blocages. La deuxième itération utilisait une file d’événements et c’était un peu plus facile à raisonner, mais ce n’est pas assez facile pour mon cerveau de le faire sans me heurter à l’impasse obscure et aux conditions de la course. La troisième et dernière itération a utilisé l'approche décrite ci-dessus, et finalement cela m'a permis de créer un cadre d'interface graphique multithread que même un simple imbécile comme moi pourrait implémenter correctement.

Ensuite, ce type de conception d’interface utilisateur multithread finale m’a permis de proposer autre chose qui était tellement plus facile à raisonner et à éviter ce type d’erreurs que j’avais tendance à commettre, et l’une des raisons pour lesquelles j’ai trouvé si facile de raisonner à Le moins, c’est à cause de ces boucles homogènes et de leur ressemblance avec le flux de contrôle, comme lorsque j’interrogeais sous DOS (bien que ce ne soit pas vraiment une interrogation et que nous n’effectuions du travail que s’il y avait du travail à faire). L’idée était de s’éloigner autant que possible du modèle de gestion des événements, ce qui implique des boucles non homogènes, des effets secondaires non homogènes, des flux de contrôle non homogènes, et de travailler de plus en plus en faveur de boucles homogènes fonctionnant de manière uniforme sur des données homogènes et isolant les données. et en unifiant les effets secondaires de manière à ce qu'il soit plus facile de se concentrer sur "quoi"


la source
1
"comme utiliser des variables de condition pour avertir les threads de se réveiller" Cela ressemble à un arrangement basé sur les événements / observateur, sans interrogation.
Josh Caswell
Je trouve la différence très subtile, mais la notification se présente simplement sous la forme "Il y a du travail à faire" pour réveiller les discussions. Par exemple, un modèle d'observateur peut, lors du redimensionnement d'un contrôle parent, redimensionner en cascade les appels en aval de la hiérarchie à l'aide de la répartition dynamique. Les fonctions d'événement de redimensionnement de ces objets seraient appelées indirectement immédiatement. Ensuite, ils pourraient se repeindre immédiatement. Ensuite, si nous utilisons une file d’événements, le redimensionnement d’un contrôle parent risque d’envoyer les événements de redimensionnement au bas de la hiérarchie. À ce stade, les fonctions de redimensionnement de chaque contrôle peuvent être appelées de manière ...
... point ils pourraient alors pousser des événements repeindre qui, de même, sont appelés de manière différée une fois que tout est terminé, et tous à partir d'un fil central de gestion des événements. Et je trouve que la centralisation est un peu bénéfique au moins en ce qui concerne le débogage et la possibilité de raisonner facilement sur le lieu du traitement (y compris le fil) ... Alors, ce que je considère être le plus proche de la scrutation n'est ni l'un ni l'autre ces solutions ...
Ce serait, par exemple, un LayoutSystemqui dort normalement, mais lorsque l'utilisateur redimensionne un contrôle, il utilise une variable de condition pour le réactiver LayoutSystem. Ensuite, le LayoutSystemredimensionne toutes les commandes nécessaires et se rendort. Au cours de ce processus, les régions rectangulaires dans lesquelles PaintSystemse trouvent les widgets sont marquées comme nécessitant des mises à jour. Un point se réveille puis passe à travers ces régions rectangulaires, en repeignant celles qui doivent être redessinées dans une boucle séquentielle plate.
Donc, la variable condition elle-même suit un modèle d'observateur pour informer les threads de se réveiller, mais nous ne transmettons aucune information au-delà de "il y a du travail à faire". Et chaque système qui se réveille est dédié au traitement des objets dans une boucle très simple appliquant une tâche très homogène, par opposition à une file d’événements qui comporte des tâches non homogènes (elle pourrait contenir un mélange éclectique d’événements à traiter).
-4

Je vous donne un aperçu plus détaillé de la manière conceptuelle de penser le modèle d'observateur. Pensez à un scénario comme s’abonner à une chaîne youtube. Un grand nombre d'utilisateurs sont abonnés au canal et une fois qu'il y a une mise à jour sur le canal qui comprend de nombreuses vidéos, l'abonné est averti qu'il y a un changement dans ce canal particulier. Nous avons donc conclu que si channel est SUBJECT, il a la capacité de s'abonner, de se désabonner et d'avertir tous les OBSERVATEURS inscrits sur le canal.

Aroop Bhattacharya
la source
2
cela n'essaye même pas de répondre à la question posée, à quel moment une interrogation d'événements serait-elle préférable à l'utilisation d'un modèle d'observateur. Voir Comment répondre
Moucheron