web-dev-qa-db-fra.com

Pourquoi les sujets ne sont-ils pas recommandés dans les extensions .NET Reactive?

Je suis actuellement en train de maîtriser le framework Reactive Extensions pour .NET et je suis en train de parcourir les différentes ressources d'introduction que j'ai trouvées (principalement http://www.introtorx.com ).

Notre application implique un certain nombre d'interfaces matérielles qui détectent les trames réseau. Ce sont mes IObservables. J'ai ensuite une variété de composants qui consomment ces trames ou effectuent une sorte de transformation sur les données et produisent un nouveau type de trame. Il y aura également d’autres composants qui devront afficher chaque ième trame, par exemple . Je suis convaincu que Rx sera utile pour notre application, mais j’ai des difficultés avec les détails de mise en œuvre de l’interface IObserver.

La plupart (sinon la totalité) des ressources que j'ai lues ont déclaré que je ne devrais pas implémenter l'interface IObservable mais utiliser l'une des fonctions ou classes fournies . Il ressort de mes recherches que la création d'un Subject<IBaseFrame> m'apporterait ce dont J'ai besoin d'un seul thread qui lit les données de l'interface matérielle puis appelle la fonction OnNext de mon instance Subject<IBaseFrame>. Les différents composants IObserver recevraient alors leurs notifications de cet objet.

Ma confusion vient des conseils donnés en annexe de ce tutoriel où il est écrit:

Évitez d'utiliser les types de sujets. Rx est effectivement un paradigme de programmation fonctionnelle. Utiliser des sujets signifie que nous gérons à présent l’état, qui est potentiellement en mutation. Il est très difficile de gérer correctement la programmation asynchrone et en mutation simultanée. En outre, de nombreux opérateurs (méthodes d'extension) ont été écrits avec soin pour garantir le maintien d'une durée de vie correcte et cohérente des souscriptions et des séquences. quand vous introduisez des sujets, vous pouvez casser ceci. Les versions futures peuvent également voir une dégradation significative des performances si vous utilisez explicitement des sujets.

Mon application est assez critique en termes de performances, je vais évidemment tester les performances de l’utilisation des modèles Rx avant qu’elle ne passe au code de production; Cependant, je crains de faire quelque chose qui va à l’encontre de l’esprit du framework Rx en utilisant la classe Subject et qu’une version future du framework nuira aux performances.

Y a-t-il une meilleure façon de faire ce que je veux? Le thread d'interrogation matériel fonctionnera en permanence, qu'il y ait ou non des observateurs (la mémoire tampon matérielle sera sauvegardée sinon), il s'agit donc d'une séquence très critique. Je dois ensuite transmettre les images reçues à plusieurs observateurs.

Tout avis serait grandement apprécié.

93
Anthony

Ok, Si nous ignorons mes voies dogmatiques et ignorons "les sujets sont bons/mauvais" tous ensemble. Regardons l'espace du problème.

Je parie que vous avez soit l'un des 2 styles de système que vous devez intégrer.

  1. Le système déclenche un événement ou un rappel lorsqu'un message arrive
  2. Vous devez interroger le système pour savoir s’il ya un message à traiter.

Pour l'option 1, facile, nous l'enveloppons simplement avec la méthode FromEvent appropriée et nous avons terminé. Au bar!

Pour l'option 2, nous devons maintenant examiner la manière dont nous interrogerons ceci et comment le faire efficacement. De plus, lorsque nous obtenons la valeur, comment la publions-nous?

J'imagine que vous voudriez un fil dédié pour le vote. Vous ne voudriez pas qu'un autre codeur martèle le ThreadPool/TaskPool et vous laisse dans une situation de famine ThreadPool. Sinon, vous ne voulez pas avoir à changer de contexte (je suppose). Supposons donc que nous avons notre propre fil, nous aurons probablement une sorte de boucle While/Sleep à interroger. Lorsque le chèque trouve des messages, nous les publions. Tout cela semble parfait pour Observable.Create. Maintenant, nous ne pourrons probablement pas utiliser une boucle While car cela ne nous permettra pas de retourner un jetable pour permettre l'annulation. Heureusement, vous avez lu tout le livre, soyez donc avertis avec la planification récursive!

J'imagine que quelque chose comme cela pourrait fonctionner. #Pas testé

public class MessageListener
{
    private readonly IObservable<iMessage> _messages;
    private readonly IScheduler _scheduler;

    public MessageListener()
    {
        _scheduler = new EventLoopScheduler();

        var messages = ListenToMessages()
                                    .SubscribeOn(_scheduler)
                                    .Publish();

        _messages = messages;
        messages.Connect();
    }

    public IObservable<iMessage> Messages
    {
        get {return _messages;}
    }

    private IObservable<iMessage> ListenToMessages()
    {
        return Observable.Create<iMessage>(o=>
        {
                return _scheduler.Schedule(recurse=>
                {
                    try
                    {           
                        var messages = GetMessages();
                        foreach (var msg in messages)
                        {
                            o.OnNext(msg);
                        }   
                        recurse();
                    }
                    catch (Exception ex)
                    {
                        o.OnError(ex);
                    }                   
                });
        });
    }

    private IEnumerable<iMessage> GetMessages()
    {
         //Do some work here that gets messages from a queue, 
         // file system, database or other system that cant Push 
         // new data at us.
         // 
         //This may return an empty result when no new data is found.
    }
}

La raison pour laquelle je n'aime vraiment pas Subjects, c'est que le développeur n'a généralement pas une conception claire du problème. Hack dans un sujet, piquez ici ici et partout, puis laissez le devin de support pauvre deviner à WTF se passait. Lorsque vous utilisez les méthodes Create/Generate etc., vous localisez les effets sur la séquence. Vous pouvez voir tout cela d'une seule manière et vous savez que personne d'autre n'a un effet secondaire désagréable. Si je vois un domaine, je dois maintenant rechercher tous les lieux de la classe où il est utilisé. Si un MFer en expose un publiquement, alors tous les paris sont ouverts, qui sait comment cette séquence est utilisée! Async/Concurrency/Rx est difficile. Vous n'avez pas besoin de rendre la tâche plus difficile en permettant aux programmes d'effets secondaires et de causalité de faire tourner votre tête encore plus.

60
Lee Campbell

En général, vous devriez éviter d'utiliser Subject. Cependant, je pense que les choses que vous faites ici fonctionnent assez bien. J'ai posé une question similaire lorsque je suis tombé sur le message "éviter les sujets" dans les tutoriels Rx. 

Pour citer Dave Sexton (de Rxx)

"Les sujets sont les composants avec état de Rx. Ils sont utiles lorsque Vous devez créer un observable semblable à un événement sous forme de champ ou de variable Locale."

J'ai tendance à les utiliser comme point d'entrée dans Rx. Donc, si j'ai du code qui doit dire «quelque chose est arrivé» (comme vous l'avez fait), j'utiliserais un Subject et appellerais OnNext. Puis exposez-le en tant que IObservable_pour que d'autres puissent s'y abonner (vous pouvez utiliser AsObservable() sur votre sujet pour vous assurer que personne ne peut créer un sujet et gâcher ses choses). 

Vous pouvez également atteindre cet objectif avec un événement .NET et utiliser FromEventPattern, mais si je ne veux que transformer l'événement en un IObservable, je ne vois pas l'intérêt d'avoir un événement au lieu d'un Subject (ce qui pourrait signifier manque quelque chose ici)

Cependant, ce que vous devriez éviter assez fortement, c’est de vous abonner à un IObservable avec un Subject, c’est-à-dire ne pas passer un Subject dans la méthode IObservable.Subscribe.

28
Wilka

Souvent, lorsque vous gérez un sujet, vous ne faites que réimplémenter des fonctionnalités déjà dans Rx, et probablement de manière moins robuste, simple et extensible.

Lorsque vous essayez d'adapter un flux de données asynchrone dans Rx (ou créez un flux de données asynchrone à partir d'un flux qui n'est pas actuellement asynchrone), les cas les plus courants sont les suivants:

  • La source de données est un événement : Comme le dit Lee, c'est le cas le plus simple: utilisez FromEvent et dirigez-vous vers le pub.

  • La source des données provient d'une opération synchrone et vous souhaitez des mises à jour interrogées , (par exemple, un service Web ou un appel à une base de données): dans ce cas, vous pouvez utiliser l'approche suggérée par Lee ou, dans le cas contraire, utiliser Observable.Interval.Select(_ => <db fetch>). Vous souhaiterez peut-être utiliser DistinctUntilChanged () pour empêcher la publication de mises à jour lorsque rien n'a changé dans les données source.

  • La source de données est une sorte d'API asynchrone qui appelle votre rappel : Dans ce cas, utilisez Observable.Create pour connecter votre rappel à l'appel OnNext/OnError/OnComplete sur l'observateur.

  • La source de données est un appel qui bloque jusqu'à ce que de nouvelles données soient disponibles (par exemple, certaines opérations de lecture de socket synchrones): dans ce cas, vous pouvez utiliser Observable.Create pour envelopper le code impératif lu depuis le socket et publié vers Observer.OnNext lors de la lecture des données. Cela peut ressembler à ce que vous faites avec le sujet.

Utiliser Observable.Create et créer une classe qui gère un sujet équivaut assez à utiliser le mot clé de rendement et créer une classe entière qui implémente IEnumerator. Bien sûr, vous pouvez écrire un IEnumerator pour être aussi propre et aussi bon citoyen que le code de rendement, mais lequel est le mieux encapsulé et a un design plus net? Il en va de même pour Observable.Create vs Managing Subjects.

Observable.Create vous donne un modèle propre pour une installation paresseuse et un démontage complet. Comment y parvenir avec une classe enveloppant un sujet? Vous avez besoin d'une sorte de méthode de démarrage ... comment savoir quand l'appeler? Ou est-ce que vous commencez toujours, même lorsque personne n’écoute? Et quand vous avez terminé, comment obtenez-vous qu'il arrête de lire dans le socket/d'interroger la base de données, etc.? Vous devez avoir une sorte de méthode Stop et vous devez toujours avoir accès non seulement à IObservable auquel vous êtes abonné, mais à la classe qui a créé le sujet en premier lieu.

Avec Observable.Create, tout est réuni au même endroit. Le corps de Observable.Create n'est pas exécuté tant que quelqu'un n'est pas abonné. Par conséquent, si personne ne s'abonne, vous n'utilisez jamais votre ressource. Et Observable.Create renvoie un objet jetable capable d’arrêter proprement vos ressources/rappels, etc. - ceci est appelé lorsque l’observateur se désabonne. La durée de vie des ressources que vous utilisez pour générer l'observable est parfaitement liée à la durée de vie de l'observable lui-même.

23
Niall Connaughton

Le texte de bloc cité explique assez bien pourquoi vous ne devriez pas utiliser Subject<T> , mais pour simplifier, vous combinez les fonctions observateur et observable, tout en injectant une sorte d’état entre les deux (que vous soyez encapsuleur ou non). ou s'étendant).

C'est là que vous rencontrez des problèmes. ces responsabilités doivent être séparées et distinctes les unes des autres.

Cela dit, dans votre cas spécifique, je vous recommanderais de diviser vos préoccupations en parties plus petites.

Tout d’abord, votre thread est très actif et surveillez toujours le matériel afin de détecter les signaux pour lesquels des notifications sont émises. Comment feriez-vous cela normalement? Événements . Commençons donc avec ça.

Définissons la EventArgs que votre événement déclenchera.

// The event args that has the information.
public class BaseFrameEventArgs : EventArgs
{
    public BaseFrameEventArgs(IBaseFrame baseFrame)
    {
        // Validate parameters.
        if (baseFrame == null) throw new ArgumentNullException("IBaseFrame");

        // Set values.
        BaseFrame = baseFrame;
    }

    // Poor man's immutability.
    public IBaseFrame BaseFrame { get; private set; }
}

Maintenant, la classe qui déclenchera l'événement. Notez qu'il peut s'agir d'une classe statique (car vous avez toujours un thread qui surveille le tampon matériel) ou de quelque chose que vous appelez à la demande et qui s'abonne à that. Vous devrez modifier cela comme il convient.

public class BaseFrameMonitor
{
    // You want to make this access thread safe
    public event EventHandler<BaseFrameEventArgs> HardwareEvent;

    public BaseFrameMonitor()
    {
        // Create/subscribe to your thread that
        // drains hardware signals.
    }
}

Alors maintenant, vous avez une classe qui expose un événement. Les observables fonctionnent bien avec les événements. À tel point qu'il existe un support de premier ordre pour la conversion des flux d'événements (pensez à un flux d'événements comme plusieurs déclenchements d'un événement) en IObservable<T> si vous suivez le modèle d'événement standard, via la méthode static FromEventPattern sur la Observable classe .

Avec la source de vos événements et la méthode FromEventPattern, nous pouvons créer facilement un IObservable<EventPattern<BaseFrameEventArgs>> (le EventPattern<TEventArgs> class incarne ce que vous verriez dans un événement .NET, notamment une instance dérivée de EventArgs et un objet représentant le expéditeur), comme suit:

// The event source.
// Or you might not need this if your class is static and exposes
// the event as a static event.
var source = new BaseFrameMonitor();

// Create the observable.  It's going to be hot
// as the events are hot.
IObservable<EventPattern<BaseFrameEventArgs>> observable = Observable.
    FromEventPattern<BaseFrameEventArgs>(
        h => source.HardwareEvent += h,
        h => source.HardwareEvent -= h);

Bien sûr, vous voulez un IObservable<IBaseFrame>, mais c’est facile, en utilisant la méthode d’extension Select sur la classe Observable pour créer une projection (comme vous le feriez dans LINQ, et nous pouvons résumer tout cela dans un document facile à utiliser. méthode d'utilisation):

public IObservable<IBaseFrame> CreateHardwareObservable()
{
    // The event source.
    // Or you might not need this if your class is static and exposes
    // the event as a static event.
    var source = new BaseFrameMonitor();

    // Create the observable.  It's going to be hot
    // as the events are hot.
    IObservable<EventPattern<BaseFrameEventArgs>> observable = Observable.
        FromEventPattern<BaseFrameEventArgs>(
            h => source.HardwareEvent += h,
            h => source.HardwareEvent -= h);

    // Return the observable, but projected.
    return observable.Select(i => i.EventArgs.BaseFrame);
}
9
casperOne

Il est mauvais de généraliser que les sujets ne sont pas bons à utiliser pour une interface publique . Bien que ce ne soit certainement pas vrai, une approche de programmation réactive ne devrait pas ressembler à une telle approche, c'est définitivement une bonne option d'amélioration/de refactorisation pour votre code classique.

Si vous avez une propriété normale avec un accesseur d'ensembles public et que vous souhaitez notifier les modifications, rien ne s'oppose à ce qu'elles soient remplacées par un BehaviorSubject . INPC ou d'autres événements supplémentaires ne sont tout simplement pas si propres et cela m'efface personnellement . À cette fin, vous pouvez et devez utiliser BehaviorSubjects en tant que propriétés publiques au lieu des propriétés normales et abandonner INPC ou d'autres événements. 

De plus, Subject-interface permet aux utilisateurs de votre interface de mieux connaître la fonctionnalité de vos propriétés et est plus susceptible de s'abonner au lieu de simplement obtenir la valeur.

Il est préférable de l'utiliser si vous voulez que les autres écoutent/souscrivent aux modifications d'une propriété.

0
Felix Keil