web-dev-qa-db-fra.com

ObserveOn et SubscribeOn - où le travail est effectué

Basé sur la lecture de cette question: Quelle est la différence entre SubscribeOn et ObserveOn

ObserveOn définit où se trouve le code dans le gestionnaire Subscribe est exécuté:

stream.Subscribe(_ => { // this code here });

La méthode SubscribeOn définit sur quel thread la configuration du flux est effectuée.

Je suis amené à comprendre que si ceux-ci ne sont pas définis explicitement, alors le TaskPool est utilisé.

Maintenant, ma question est, disons que je fais quelque chose comme ça:

Observable.Interval(new Timespan(0, 0, 1)).Where(t => predicate(t)).SelectMany(t => lots_of(t)).ObserveOnDispatcher().Subscribe(t => some_action(t));

Où sont les Wherepredicate et SelectManylots_of en cours d'exécution, étant donné que some_action est en cours d'exécution sur le répartiteur?

53
Cheetah

Il y a beaucoup d'informations trompeuses sur SubscribeOn et ObserveOn.

Sommaire

  • SubscribeOn intercepte les appels à la méthode unique de IObservable<T>, qui est Subscribe, et appelle Dispose sur la poignée IDisposable renvoyée par Subscribe.
  • ObserveOn intercepte les appels aux méthodes de IObserver<T>, qui sont OnNext, OnCompleted & OnError.
  • Les deux méthodes entraînent les appels respectifs sur le planificateur spécifié.

Analyses et démonstrations

La déclaration

ObserveOn définit où le code du gestionnaire d'abonnement est exécuté:

est plus déroutant qu'utile. Ce que vous appelez le "gestionnaire d'abonnement" est en fait un gestionnaire OnNext. N'oubliez pas que la méthode Subscribe de IObservable accepte un IObserver qui a OnNext, OnCompleted et OnError, mais ce sont des méthodes d'extension qui fournissent les surcharges pratiques qui acceptent les lambdas et créent une implémentation IObserver pour vous.

Permettez-moi cependant de reprendre le terme; Je pense que le "gestionnaire d'abonnement" est le code dans l'observable qui est appelé lorsque Subscribe est appelé. De cette façon, la description ci-dessus ressemble plus à l'objectif de SubscribeOn.

Abonnez-vous

SubscribeOn provoque l'exécution asynchrone de la méthode Subscribe d'un observable sur le planificateur ou le contexte spécifié. Vous l'utilisez lorsque vous ne voulez pas appeler la méthode Subscribe sur un observable à partir de n'importe quel thread sur lequel vous exécutez - généralement parce qu'elle peut être longue et que vous ne voulez pas bloquer le thread appelant.

Lorsque vous appelez Subscribe, vous appelez un observable qui peut faire partie d'une longue chaîne d'observables. Ce n'est que l'observable auquel SubscribeOn est appliqué qu'il affecte. Maintenant, il se peut que tous les observables de la chaîne soient abonnés immédiatement et sur le même fil - mais cela ne doit pas être le cas. Pensez à Concat par exemple - qui ne s'abonne à chaque flux successif qu'une fois le flux précédent terminé, et cela aura généralement lieu sur le thread à partir duquel le flux précédent appelé OnCompleted.

Ainsi, SubscribeOn se situe entre votre appel à Subscribe et l'observable auquel vous êtes abonné, interceptant l'appel et le rendant asynchrone.

Elle affecte également l'élimination des abonnements. Subscribe renvoie une poignée IDisposable qui est utilisée pour se désinscrire. SubscribeOn garantit que les appels à Dispose sont planifiés sur le planificateur fourni.

Un point commun de confusion lorsque l'on essaie de comprendre ce que fait SubscribeOn est que le gestionnaire Subscribe d'un observable peut bien appeler OnNext, OnCompleted ou OnError sur ce même fil. Cependant, son but n'est pas d'affecter ces appels. Il n'est pas rare qu'un flux se termine avant le retour de la méthode Subscribe. Observable.Return fait cela, par exemple. Nous allons jeter un coup d'oeil.

Si vous utilisez la méthode Spy que j'ai écrite et exécutez le code suivant:

Console.WriteLine("Calling from Thread: " + Thread.CurrentThread.ManagedThreadId);
var source = Observable.Return(1).Spy("Return");
source.Subscribe();
Console.WriteLine("Subscribe returned");

Vous obtenez cette sortie (l'ID du thread peut varier bien sûr):

Calling from Thread: 1
Return: Observable obtained on Thread: 1
Return: Subscribed to on Thread: 1
Return: OnNext(1) on Thread: 1
Return: OnCompleted() on Thread: 1
Return: Subscription completed.
Subscribe returned

Vous pouvez voir que l'intégralité du gestionnaire d'abonnement s'est exécuté sur le même thread et s'est terminé avant de revenir.

Utilisons SubscribeOn pour exécuter cela de manière asynchrone. Nous allons espionner à la fois l'observable Return et l'observable SubscribeOn:

Console.WriteLine("Calling from Thread: " + Thread.CurrentThread.ManagedThreadId);
var source = Observable.Return(1).Spy("Return");
source.SubscribeOn(Scheduler.Default).Spy("SubscribeOn").Subscribe();
Console.WriteLine("Subscribe returned");

Cela produit (numéros de ligne ajoutés par moi):

01 Calling from Thread: 1
02 Return: Observable obtained on Thread: 1
03 SubscribeOn: Observable obtained on Thread: 1
04 SubscribeOn: Subscribed to on Thread: 1
05 SubscribeOn: Subscription completed.
06 Subscribe returned
07 Return: Subscribed to on Thread: 2
08 Return: OnNext(1) on Thread: 2
09 SubscribeOn: OnNext(1) on Thread: 2
10 Return: OnCompleted() on Thread: 2
11 SubscribeOn: OnCompleted() on Thread: 2
12 Return: Subscription completed.

01 - La méthode principale est en cours d'exécution sur le thread 1.

02 - l'observable Return est évalué sur le thread appelant. Nous obtenons juste le IObservable ici, rien n'est encore souscrit.

03 - L'observable SubscribeOn est évalué sur le thread appelant.

04 - Maintenant nous appelons enfin la méthode Subscribe de SubscribeOn.

05 - La méthode Subscribe se termine de manière asynchrone ...

06 - ... et le thread 1 revient à la méthode principale. C'est l'effet de SubscribeOn en action!

07 - Pendant ce temps, SubscribeOn a planifié un appel sur le planificateur par défaut vers Return. Ici, il est reçu sur le fil 2.

08 - Et comme Return le fait, il appelle OnNext sur le thread Subscribe ...

09 - et SubscribeOn n'est plus qu'un passage maintenant.

10,11 - Idem pour OnCompleted

12 - Et enfin le gestionnaire d'abonnement Return est terminé.

Espérons que cela clarifie le but et l'effet de SubscribeOn!

ObserveOn

Si vous pensez à SubscribeOn comme un intercepteur pour la méthode Subscribe qui transmet l'appel à un autre thread, alors ObserveOn fait le même travail, mais pour le OnNext, OnCompleted et OnError appels.

Rappelons notre exemple original:

Console.WriteLine("Calling from Thread: " + Thread.CurrentThread.ManagedThreadId);
var source = Observable.Return(1).Spy("Return");
source.Subscribe();
Console.WriteLine("Subscribe returned");

Ce qui a donné cette sortie:

Calling from Thread: 1
Return: Observable obtained on Thread: 1
Return: Subscribed to on Thread: 1
Return: OnNext(1) on Thread: 1
Return: OnCompleted() on Thread: 1
Return: Subscription completed.
Subscribe returned

Modifions maintenant ceci pour utiliser ObserveOn:

Console.WriteLine("Calling from Thread: " + Thread.CurrentThread.ManagedThreadId);
var source = Observable.Return(1).Spy("Return");
source.ObserveOn(Scheduler.Default).Spy("ObserveOn").Subscribe();
Console.WriteLine("Subscribe returned");

Nous obtenons la sortie suivante:

01 Calling from Thread: 1
02 Return: Observable obtained on Thread: 1
03 ObserveOn: Observable obtained on Thread: 1
04 ObserveOn: Subscribed to on Thread: 1
05 Return: Subscribed to on Thread: 1
06 Return: OnNext(1) on Thread: 1
07 ObserveOn: OnNext(1) on Thread: 2
08 Return: OnCompleted() on Thread: 1
09 Return: Subscription completed.
10 ObserveOn: Subscription completed.
11 Subscribe returned
12 ObserveOn: OnCompleted() on Thread: 2

01 - La méthode principale est en cours d'exécution sur le thread 1.

02 - Comme précédemment, l'observable Return est évalué sur le thread appelant. Nous obtenons juste le IObservable ici, rien n'est encore souscrit.

03 - L'observable ObserveOn est également évalué sur le thread appelant.

04 - Maintenant, nous souscrivons, toujours sur le thread appelant, d'abord au ObserveOn observable ...

05 - ... qui transmet ensuite l'appel à l'observable Return.

06 - Maintenant Return appelle OnNext dans son gestionnaire Subscribe.

07 - Voici l'effet de ObserveOn. Nous pouvons voir que le OnNext est planifié de manière asynchrone sur le Thread 2.

08 - Pendant ce temps Return appelle OnCompleted sur le Thread 1 ...

09 - Et le gestionnaire d'abonnement de Return se termine ...

10 - et puis le gestionnaire d'abonnement de ObserveOn ...

11 - donc le contrôle revient à la méthode principale

12 - Pendant ce temps, ObserveOn a fait la navette Return's OnCompleted appelez cela sur le Thread 2. Cela aurait pu arriver à tout moment pendant le 09-11 car il fonctionne de manière asynchrone. Il se trouve que c'est finalement appelé maintenant.

Quels sont les cas d'utilisation typiques?

Vous verrez le plus souvent SubscribeOn utilisé dans une interface graphique lorsque vous avez besoin de Subscribe pour un observable de longue durée et que vous souhaitez quitter le thread du répartiteur dès que possible - peut-être parce que vous savez que c'est l'un des ces observables qui font tout son travail dans le gestionnaire d'abonnement. Appliquez-le à la fin de la chaîne observable, car il s'agit du premier observable appelé lorsque vous vous abonnez.

Vous verrez le plus souvent ObserveOn utilisé dans une interface graphique lorsque vous voulez vous assurer que les appels OnNext, OnCompleted et OnError sont réorganisés vers le thread du répartiteur. Appliquez-le à la fin de la chaîne observable pour revenir le plus tard possible.

J'espère que vous pouvez voir que la réponse à votre question est que ObserveOnDispatcher ne fera aucune différence pour les threads sur lesquels Where et SelectMany sont exécutés - tout dépend de quel thread stream les appelle depuis! Le gestionnaire d'abonnement de stream sera invoqué sur le thread appelant, mais il est impossible de dire où Where et SelectMany s'exécuteront sans savoir comment stream est implémenté.

Observables avec des durées de vie qui survivent à l'appel d'abonnement

Jusqu'à présent, nous nous sommes penchés exclusivement sur Observable.Return. Return termine son flux dans le gestionnaire Subscribe. Ce n'est pas atypique, mais il est tout aussi courant que les flux survivent au gestionnaire Subscribe. Regarder Observable.Timer par exemple:

Console.WriteLine("Calling from Thread: " + Thread.CurrentThread.ManagedThreadId);
var source = Observable.Timer(TimeSpan.FromSeconds(1)).Spy("Timer");
source.Subscribe();
Console.WriteLine("Subscribe returned");

Cela renvoie les éléments suivants:

Calling from Thread: 1
Timer: Observable obtained on Thread: 1
Timer: Subscribed to on Thread: 1
Timer: Subscription completed.
Subscribe returned
Timer: OnNext(0) on Thread: 2
Timer: OnCompleted() on Thread: 2

Vous pouvez clairement voir l'abonnement à terminer, puis OnNext et OnCompleted être appelé plus tard sur un thread différent.

Notez qu'aucune combinaison de SubscribeOn ou ObserveOn n'aura quelque effet que ce soit sur quel thread ou ordonnanceur Timer choisit d'invoquer OnNext et OnCompleted on.

Bien sûr, vous pouvez utiliser SubscribeOn pour déterminer le thread Subscribe:

Console.WriteLine("Calling from Thread: " + Thread.CurrentThread.ManagedThreadId);
var source = Observable.Timer(TimeSpan.FromSeconds(1)).Spy("Timer");
source.SubscribeOn(NewThreadScheduler.Default).Spy("SubscribeOn").Subscribe();
Console.WriteLine("Subscribe returned");

(Je passe délibérément au NewThreadScheduler ici pour éviter toute confusion dans le cas où Timer se produit pour obtenir le même thread de pool de threads que SubscribeOn)

Donnant:

Calling from Thread: 1
Timer: Observable obtained on Thread: 1
SubscribeOn: Observable obtained on Thread: 1
SubscribeOn: Subscribed to on Thread: 1
SubscribeOn: Subscription completed.
Subscribe returned
Timer: Subscribed to on Thread: 2
Timer: Subscription completed.
Timer: OnNext(0) on Thread: 3
SubscribeOn: OnNext(0) on Thread: 3
Timer: OnCompleted() on Thread: 3
SubscribeOn: OnCompleted() on Thread: 3

Ici, vous pouvez clairement voir le thread principal sur le thread (1) renvoyer après ses appels Subscribe, mais l'abonnement Timer obtient son propre thread (2), mais le OnNext et OnCompleted appels exécutés sur le thread (3).

Maintenant pour ObserveOn, changeons le code en (pour ceux qui suivent dans le code, utilisez le paquet nuget rx-wpf):

var dispatcher = Dispatcher.CurrentDispatcher;
Console.WriteLine("Calling from Thread: " + Thread.CurrentThread.ManagedThreadId);
var source = Observable.Timer(TimeSpan.FromSeconds(1)).Spy("Timer");
source.ObserveOnDispatcher().Spy("ObserveOn").Subscribe();
Console.WriteLine("Subscribe returned");

Ce code est un peu différent. La première ligne garantit que nous avons un répartiteur, et nous apportons également ObserveOnDispatcher - c'est comme ObserveOn, sauf qu'il spécifie que nous devons utiliser le DispatcherScheduler de quel que soit le thread ObserveOnDispatcher est évalué.

Ce code donne la sortie suivante:

Calling from Thread: 1
Timer: Observable obtained on Thread: 1
ObserveOn: Observable obtained on Thread: 1
ObserveOn: Subscribed to on Thread: 1
Timer: Subscribed to on Thread: 1
Timer: Subscription completed.
ObserveOn: Subscription completed.
Subscribe returned
Timer: OnNext(0) on Thread: 2
ObserveOn: OnNext(0) on Thread: 1
Timer: OnCompleted() on Thread: 2
ObserveOn: OnCompleted() on Thread: 1

Notez que le répartiteur (et le thread principal) sont le thread 1. Timer appelle toujours OnNext et OnCompleted sur le thread de son choix (2) - mais le ObserveOnDispatcher rassemble les appels sur le thread du répartiteur, thread (1).

Notez également que si nous devions bloquer le thread du répartiteur (disons par un Thread.Sleep) vous verriez que ObserveOnDispatcher se bloquerait (ce code fonctionne mieux dans une méthode principale LINQPad):

var dispatcher = Dispatcher.CurrentDispatcher;
Console.WriteLine("Calling from Thread: " + Thread.CurrentThread.ManagedThreadId);
var source = Observable.Timer(TimeSpan.FromSeconds(1)).Spy("Timer");
source.ObserveOnDispatcher().Spy("ObserveOn").Subscribe();
Console.WriteLine("Subscribe returned");
Console.WriteLine("Blocking the dispatcher");
Thread.Sleep(2000);
Console.WriteLine("Unblocked");

Et vous verrez une sortie comme celle-ci:

Calling from Thread: 1
Timer: Observable obtained on Thread: 1
ObserveOn: Observable obtained on Thread: 1
ObserveOn: Subscribed to on Thread: 1
Timer: Subscribed to on Thread: 1
Timer: Subscription completed.
ObserveOn: Subscription completed.
Subscribe returned
Blocking the dispatcher
Timer: OnNext(0) on Thread: 2
Timer: OnCompleted() on Thread: 2
Unblocked
ObserveOn: OnNext(0) on Thread: 1
ObserveOn: OnCompleted() on Thread: 1

Avec les appels via le ObserveOnDispatcher, il ne peut sortir que lorsque le Sleep a été exécuté.

Points clés

Il est utile de garder à l'esprit que Reactive Extensions est essentiellement une bibliothèque à thread libre et essaie d'être aussi paresseux que possible sur le thread sur lequel il s'exécute - vous devez délibérément interférer avec ObserveOn, SubscribeOn et en passant des ordonnanceurs spécifiques aux opérateurs qui les acceptent pour changer cela.

Il n'y a rien qu'un consommateur d'un observable puisse faire pour contrôler ce qu'il fait en interne - ObserveOn et SubscribeOn sont décorateurs qui enveloppent la surface des observateurs et des observables pour rassembler les appels à travers les fils. Espérons que ces exemples l’ont montré clairement.

168
James World

J'ai trouvé la réponse de James très claire et complète. Cependant, malgré cela, je dois encore expliquer les différences.

Par conséquent, j'ai créé un exemple très simple/stupide qui me permet de démontrer graphiquement à quels ordonnanceurs les choses sont appelées. J'ai créé une classe MyScheduler qui exécute les actions immédiatement, mais va changer la couleur de la console.

Le texte sorti du planificateur SubscribeOn est affiché en rouge et celui du planificateur ObserveOn est affiché en bleu.

using System;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Reactive.Linq;

namespace SchedulerExample
{

    class Program
    {
        static void Main(string[] args)
        {
            var mydata = new[] {"A", "B", "C", "D", "E"};
            var observable = Observable.Create<string>(observer =>
                                            {
                                                Console.WriteLine("Observable.Create");
                                                return mydata.ToObservable().
                                                    Subscribe(observer);
                                            });

            observable.
                SubscribeOn(new MyScheduler(ConsoleColor.Red)).
                ObserveOn(new MyScheduler(ConsoleColor.Blue)).
                Subscribe(s => Console.WriteLine("OnNext {0}", s));

            Console.ReadKey();
        }
    }
}

Cela produit:

scheduler

Et pour référence MyScheduler (ne convient pas pour une utilisation réelle):

using System;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;

namespace SchedulerExample
{
    class MyScheduler : IScheduler
    {
        private readonly ConsoleColor _colour;

        public MyScheduler(ConsoleColor colour)
        {
            _colour = colour;
        }

        public IDisposable Schedule<TState>(TState state, Func<IScheduler, TState, IDisposable> action)
        {
            return Execute(state, action);
        }

        private IDisposable Execute<TState>(TState state, Func<IScheduler, TState, IDisposable> action)
        {
            var tmp = Console.ForegroundColor;
            Console.ForegroundColor = _colour;
            action(this, state);
            Console.ForegroundColor = tmp;
            return Disposable.Empty;
        }

        public IDisposable Schedule<TState>(TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action)
        {
            throw new NotImplementedException();
        }

        public IDisposable Schedule<TState>(TState state, DateTimeOffset dueTime, Func<IScheduler, TState, IDisposable> action)
        {
            throw new NotImplementedException();
        }

        public DateTimeOffset Now
        {
            get { return DateTime.UtcNow; }
        }
    }
}
13
Dave Hillier

Je me trompe souvent que .SubcribeOn est utilisé pour définir le thread où le code à l'intérieur .Subscribe est en cours d'exécution. Mais pour se souvenir, pensez simplement que la publication et l'abonnement doivent être des paires comme le yin-yang. Pour définir où Subscribe's code en cours d'exécution, utilisez ObserveOn. Pour définir où Observable's code exécuté utilisé SubscribeOn. Ou en résumé: where-what, Subscribe-Observe, Observe-Subscribe.

0
o0omycomputero0o