Différence entre consommateur / producteur et observateur / observable

15

Je travaille sur la conception d'une application composée de trois parties:

  • un seul thread qui surveille certains événements qui se produisent (création de fichiers, demandes externes, etc.)
  • N threads de travail qui répondent à ces événements en les traitant (chaque travailleur traite et consomme un seul événement et le traitement peut prendre un temps variable)
  • un contrôleur qui gère ces threads et gère les erreurs (redémarrage des threads, journalisation des résultats)

Bien que ce soit assez basique et pas difficile à implémenter, je me demande quelle serait la "bonne" façon de le faire (dans ce cas concret en Java, mais des réponses d'abstraction plus élevées sont également appréciées). Deux stratégies me viennent à l'esprit:

  • Observateur / Observable: le fil d'observation est observé par le contrôleur. En cas d'événement, le contrôleur est alors notifié et peut affecter la nouvelle tâche à un thread libre à partir d'un pool de threads mis en cache réutilisable (ou attendre et mettre en cache les tâches dans la file d'attente FIFO si tous les threads sont actuellement occupés). Les threads de travail implémentent Callable et retournent avec succès le résultat (ou une valeur booléenne), ou retournent avec une erreur, auquel cas le contrôleur peut décider quoi faire (selon la nature de l'erreur qui s'est produite).

  • Producteur / consommateur : le thread de surveillance partage une BlockingQueue avec le contrôleur (file d'attente d'événements) et le contrôleur en partage deux avec tous les travailleurs (file d'attente des tâches et file d'attente des résultats). En cas d'événement, le thread de surveillance place un objet de tâche dans la file d'attente des événements. Le contrôleur prend de nouvelles tâches dans la file d'attente des événements, les examine et les place dans la file d'attente des tâches. Chaque travailleur attend de nouvelles tâches et les prend / les consomme dans la file d'attente des tâches (premier arrivé, premier servi, géré par la file d'attente elle-même), remettant les résultats ou les erreurs dans la file d'attente des résultats. Enfin, le contrôleur peut récupérer les résultats de la file d'attente de résultats et prendre les mesures appropriées en cas d'erreurs.

Les résultats finaux des deux approches sont similaires, mais ils présentent chacun de légères différences:

Avec Observers, le contrôle des threads est direct et chaque tâche est attribuée à un nouveau travailleur spécifique. Les frais généraux pour la création de threads peuvent être plus élevés, mais pas beaucoup grâce au pool de threads mis en cache. En revanche, le modèle Observer est réduit à un seul Observateur au lieu de plusieurs, ce qui n'est pas exactement ce pour quoi il a été conçu.

La stratégie de file d'attente semble être plus facile à étendre, par exemple, l'ajout de plusieurs producteurs au lieu d'un est simple et ne nécessite aucune modification. L'inconvénient est que tous les threads s'exécuteraient indéfiniment, même lorsqu'ils ne font aucun travail, et la gestion des erreurs / résultats ne semble pas aussi élégante que dans la première solution.

Quelle serait l'approche la plus appropriée dans cette situation et pourquoi? J'ai trouvé difficile de trouver des réponses à cette question en ligne, car la plupart des exemples ne concernent que des cas clairs, comme la mise à jour de nombreuses fenêtres avec une nouvelle valeur dans le cas Observer ou le traitement avec plusieurs consommateurs et producteurs. Toute contribution est grandement appréciée.

user183536
la source

Réponses:

10

Vous êtes tout près de répondre à votre propre question. :)

Dans le modèle Observable / Observer (notez le flip), il y a trois choses à garder à l'esprit:

  1. Généralement, la notification du changement, c'est-à-dire «charge utile», est dans l'observable.
  2. L'observable existe .
  3. Les observateurs doivent être connus de l' observable existant (sinon ils n'ont rien à observer).

En combinant ces points, cela implique que l'observable sait quelles sont ses composantes en aval, c'est-à-dire les observateurs. Le flux de données est intrinsèquement tiré de l'observable - les observateurs ne font que «vivre et mourir» en fonction de ce qu'ils observent.

Dans le modèle Producteur / Consommateur, vous obtenez une interaction très différente:

  1. Généralement, la charge utile existe indépendamment du producteur responsable de sa production.
  2. Les producteurs ne savent pas comment ni quand les consommateurs sont actifs.
  3. Les consommateurs n'ont pas besoin de connaître le producteur de la charge utile.

Le flux de données est maintenant complètement coupé entre un producteur et un consommateur - tout ce que le producteur sait, c'est qu'il a une sortie, et tout ce que le consommateur sait, c'est qu'il a une entrée. Surtout, cela signifie que les producteurs et les consommateurs peuvent exister entièrement sans la présence de l'autre.

Une autre différence pas si subtile est que plusieurs observateurs sur le même observable reçoivent généralement la même charge utile (sauf s'il y a une mise en œuvre non conventionnelle), alors que plusieurs consommateurs du même producteur ne le peuvent pas. Cela dépend si l'intermédiaire est une approche de type file d'attente ou sujet. Le premier transmet un message différent pour chaque consommateur, tandis que le second garantit (ou tente) que tous les consommateurs traitent message par message.

Pour les adapter à votre application:

  • Dans le modèle Observable / Observer, chaque fois que votre thread de veille s'initialise, il doit savoir comment informer le contrôleur. En tant qu'observateur, le contrôleur attend probablement une notification du thread de surveillance avant de laisser les threads gérer le changement.
  • Dans le modèle Producteur / Consommateur, votre thread de surveillance n'a besoin que de connaître la présence de la file d'attente des événements et interagit uniquement avec cela. En tant que consommateur, le contrôleur interroge ensuite la file d'attente d'événements et une fois qu'il obtient une nouvelle charge utile, il laisse les threads la gérer.

Par conséquent, pour répondre directement à votre question: si vous souhaitez maintenir un certain niveau de séparation entre votre fil d'observation et votre contrôleur de sorte que vous puissiez les faire fonctionner indépendamment, vous devez tendre vers le modèle Producteur / Consommateur.

hjk
la source
2
Merci pour votre réponse détaillée. Malheureusement, je ne peux pas le voter en raison de sa réputation manquante, je l'ai donc plutôt indiquée comme solution. L'indépendance temporelle entre les deux parties dont vous avez parlé est quelque chose de positif auquel je n'ai pas pensé jusqu'à présent. Les files d'attente peuvent gérer de courtes rafales de nombreux événements avec de longues pauses entre bien mieux que l'action directe après que les événements ont été observés (si le nombre maximal de threads est fixe et relativement faible). Le nombre de threads peut également être augmenté / diminué dynamiquement en fonction du nombre actuel d'éléments de file d'attente.
user183536
@ user183536 Aucun problème, heureux de vous aider! :)
hjk