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 Where
predicate
et SelectMany
lots_of
en cours d'exécution, étant donné que some_action
est en cours d'exécution sur le répartiteur?
Il y a beaucoup d'informations trompeuses sur SubscribeOn
et ObserveOn
.
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
.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
.
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
!
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.
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é.
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é.
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.
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:
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; }
}
}
}
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
.