web-dev-qa-db-fra.com

RxJava, un abonné multiple observable: publish (). AutoConnect ()

Je joue avec rxJava/rxAndroid et il y a quelque chose de très basique qui ne se comporte pas comme je m'y attendais. J'ai celui-ci observable et deux abonnés:

Observable<Integer> dataStream = Observable.just(1, 2, 3).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());

Log.d(TAG, "subscribing sub1...");
dataStream.subscribe(v -> System.out.println("Subscriber #1: "+ integer));

Log.d(TAG, "subscribing sub2...");
dataStream.subscribe(v -> System.out.println("Subscriber #2: "+ integer));

Et voici la sortie:

D/RxJava: subscribing sub1...
D/RxJava: subscribing sub2...
D/RxJava: Subscriber #1: 1
D/RxJava: Subscriber #1: 2
D/RxJava: Subscriber #1: 3
D/RxJava: Subscriber #2: 1
D/RxJava: Subscriber #2: 2
D/RxJava: Subscriber #2: 3

Maintenant, je sais que je pourrais éviter de répéter le compte en utilisant publish().autoConnect() mais j'essaie d'abord de comprendre ce comportement par défaut. Chaque fois que quelqu'un s'abonne à l'observable, il commence à émettre la séquence numérique. Je comprends ça. Ainsi, lorsque Subscriber 1 Se connecte, il commence à émettre des éléments. Subscriber 2 Se connecte immédiatement, pourquoi n'obtient-il pas également les valeurs?

Voici comment je le comprends, du point de vue de l'observable:

  1. Quelqu'un s'est abonné à moi, je devrais commencer à émettre des articles
    [ABONNÉS: 1] [ARTICLES À ÉMETTRE: 1,2,3]

  2. Émettre l'élément "1" aux abonnés
    [ABONNÉS: 1] [ARTICLES À ÉMETTRE: 2,3]

  3. Quelqu'un d'autre s'est abonné à moi, j'émettrai à nouveau 1,2,3 quand j'aurai fini
    [ABONNÉS: 1 & 2] [ARTICLES À ÉMETTRE: 2,3,1,2,3]

  4. Émettre l'élément "2" aux abonnés
    [ABONNÉS: 1 & 2] [ARTICLES À ÉMETTRE: 3,1,2,3]

  5. Émettre l'élément "3" aux abonnés
    [ABONNÉS: 1 & 2] [ARTICLES À ÉMETTRE: 1,2,3]

  6. Émettre l'élément "1" aux abonnés
    [ABONNÉS: 1 & 2] [ARTICLES À ÉMETTRE: 2,3]

  7. ...

Mais ce n'est pas ainsi que cela fonctionne. C'est comme s'ils étaient deux observables distincts en un. cela me déroute, pourquoi ne donnent-ils pas les articles à tous les abonnés?

Bonus:

Comment est-ce que publish().autoConnect() résout le problème? Décomposons-le. publish() me donne un observable connectable. un observable connectable est comme un observable ordinaire mais vous pouvez lui dire quand se connecter. Ensuite, je vais lui dire de se connecter immédiatement en appelant autoConnect()

Ce faisant ... je ne reçois pas la même chose avec laquelle j'ai commencé? Un simple observable régulier. Les opérateurs semblent s'annuler.

Je pourrais simplement fermer et utiliser publish().autoconnect(). Mais j'aimerais mieux comprendre comment fonctionnent les observables.

Merci!

13
FRR

En effet, ce sont deux observables distincts. Ils sont "générés" lorsque vous appelez subscribe(). Par conséquent, les étapes que vous avez fournies sont incorrectes en ce sens que les étapes 3 et 4 ne sont que 1 et 2 mais sur un autre observable.

Mais vous les voyez comme 1 1 1 2 2 2 en raison du thread sur lequel la journalisation se produit. Si vous supprimiez la partie observeOn(), vous verriez les émissions de manière entrelacée. Pour voir ce code d'exécution ci-dessous:

@Test
public void test() throws InterruptedException {
    final Scheduler single = Schedulers.single();
    final long l = System.nanoTime();
    Observable<Long> dataStream =
            Observable.just(1, 2, 3)
                    .map(i -> System.nanoTime())
                    .subscribeOn(Schedulers.computation());
                    //.observeOn(single);

    dataStream.subscribe(i -> System.out.println("1  " + Thread.currentThread().getName() + " " + (i - l)));
    dataStream.subscribe(i -> System.out.println("2  " + Thread.currentThread().getName() + " " + (i - l)));

    Thread.sleep(1000);
}

Sortie, au moins dans ma course était (notez les noms de threads):

1  RxComputationThreadPool-1 135376988
2  RxComputationThreadPool-2 135376988
1  RxComputationThreadPool-1 135486815
2  RxComputationThreadPool-2 135537383
1  RxComputationThreadPool-1 135560691
2  RxComputationThreadPool-2 135617580

et si vous appliquez la observeOn() elle devient:

1  RxSingleScheduler-1 186656395
1  RxSingleScheduler-1 187919407
1  RxSingleScheduler-1 187923753
2  RxSingleScheduler-1 186656790
2  RxSingleScheduler-1 187860148
2  RxSingleScheduler-1 187864889

Comme vous l'avez correctement souligné, pour obtenir ce que vous voulez, vous avez besoin de l'opérateur publish().refcount() ou simplement share() (c'est un alias).

Cela est dû au fait que la publish() crée un ConnectableObservable qui ne commence pas à émettre des éléments tant qu'on ne lui a pas demandé de le faire via la méthode connect(). dans ce cas, si vous faites cela:

@Test
public void test() throws InterruptedException {
    final Scheduler single = Schedulers.single();
    final long l = System.nanoTime();
    ConnectableObservable<Long> dataStream =
            Observable.just(1, 2, 3)
                    .map(i -> System.nanoTime())
                    .subscribeOn(Schedulers.computation())
                    .observeOn(single)
                    .publish();

    dataStream.subscribe(i -> System.out.println("1  " + (i - l)));
    dataStream.subscribe(i -> System.out.println("2  " + (i - l)));

    Thread.sleep(1000);
    dataStream.connect();
    Thread.sleep(1000);

}

Vous remarquerez que pendant la première seconde (la première invocation de Thread.sleep()), rien ne se passe et juste après que la dataStream.connect() est appelée, les émissions se produisent.

refCount() prend un ConnectableObservable et cache aux abonnés la nécessité d'appeler connect() en comptant combien d'abonnés sont actuellement abonnés. Ce qu'il fait, c'est lors du premier abonnement qu'il appelle connect() et après la dernière désinscription, il se désabonne de l'observable d'origine.

Quant à l'annulation mutuelle de la publish().autoConnect(), vous obtenez ensuite un observable mais il a une propriété spéciale, disons que l'observable d'origine fait un appel d'API sur Internet (d'une durée de 10 secondes), lorsque vous l'utilisez sans share() vous vous retrouverez avec autant de requêtes parallèles au serveur qu'il y a eu d'abonnements pendant ces 10 secondes. En revanche avec share() vous n'aurez qu'un seul appel.

Vous n'en verrez aucun avantage, si un observable partagé termine son travail très rapidement (comme just(1,2,3)).

autoConnect()/refCount() vous donne un observable intermédiaire auquel vous souscrivez au lieu de l'observable d'origine.

Si vous êtes intéressé, plongez dans ce livre: Programmation réactive avec RxJava

15
MatBos

Régulier (froid) observable

Au cœur de Observable se trouve la fonction subscribe. Chaque fois qu'un nouvel observateur s'abonne, il est transmis à cette fonction en tant que paramètre. Ce que fait cette fonction, elle alimente les données dans cet observateur unique . Il le fait en appelant la méthode observer.onNext. Il peut le faire immédiatement (comme just le fait), ou via un planificateur (par exemple. interval), ou à partir d'un thread d'arrière-plan ou d'un rappel (par exemple en lançant une tâche asynchrone).

J'ai mis en surbrillance Word single ci-dessus car c'est le seul observateur que cette fonction connaisse lorsqu'elle est invoquée. Si vous vous abonnez plusieurs fois à un tel observable, sa fonction subscribe est appelée pour chaque abonné.

La source de données comme celle-ci est appelée observable à froid.

Planificateurs

L'application de l'opérateur subscribeOn ajoute une étape intermédiaire entre votre appel subscribe et la fonction subscribe de l'observable d'origine. Vous ne l'appelez plus directement, mais planifiez votre appel via le planificateur spécifié.

observeOn ajoute une étape intermédiaire similaire à toutes les invocations onNext de votre observateur.

Dans votre exemple, la fonction subscribe est appelée deux fois, c'est-à-dire que les séries de données sont générées deux fois. Les appels sont planifiés via le planificateur io multi-thread, donc ces invocations ne se produisent pas sur le thread principal mais sur deux autres threads, presque simultanément. Les deux threads commencent à invoquer les méthodes onNext de deux abonnés. N'oubliez pas que chaque thread ne connaît que son propre abonné. onNext les appels sont planifiés par mainThread ordonnanceur, qui est à thread unique, c'est-à-dire qu'ils ne peuvent pas se produire simultanément mais doivent être mis en file d'attente d'une manière ou d'une autre. Strictement parlant, il ne peut y avoir aucune garantie quant à l'ordre de ces appels. Cela dépend de divers facteurs et est spécifique à la mise en œuvre. Essayez de remplacer just par interval (cela introduira un délai entre les messages) et vous verrez que les messages arriveront dans un ordre différent.

Observable à chaud

L'opérateur publish rend votre observable chaud , alias connectable . Il ajoute des étapes intermédiaires à la fois à la fonction subscribe - cela n'est appelé qu'une seule fois, et aux méthodes onNext - celles-ci sont propagées à tous les observables abonnés. En d'autres termes, , il permet à plusieurs abonnés de partager un seul abonnement .

Pour être précis, la fonction subscribe est appelée lorsque vous appelez la méthode connect. Il existe deux opérateurs qui appellent automatiquement connect:

  • autoConnect invoque la méthode connect lorsque le premier abonné entre. Elle ne se déconnecte cependant jamais.
  • refCount invoque connect lorsque le premier abonné entre et se déconnecte automatiquement lorsque le dernier abonné se désabonne. Il se reconnectera (appelera à nouveau la fonction subscribe) lorsque de nouveaux abonnés arriveront.

publish().refCount() est une combinaison populaire, donc elle a un raccourci: share().

Pour votre éducation, essayez le code suivant avec et sans share:

Observable<Long> dataStream = Observable.interval(100, TimeUnit.MILLISECONDS)
        .take(3)
        .share();
System.out.println("subscribing A");
dataStream.subscribe(v -> System.out.println("A got " + v));
TimeUnit.MILLISECONDS.sleep(150);
System.out.println("subscribing B");
dataStream.subscribe(v -> System.out.println("B got " + v));
TimeUnit.SECONDS.sleep(1);

Réponses aux questions originales

1) Observable à froid traite toujours avec un seul abonné. Ainsi, vos diagrammes de temps doivent ressembler à ceci:

subscribed first subscriber
[SUBSCRIBER: 1][ITEMS TO EMIT: 1,2,3]
subscribed second subscriber
[SUBSCRIBER: 1][ITEMS TO EMIT: 1,2,3]
[SUBSCRIBER: 2][ITEMS TO EMIT: 1,2,3]
emit "1" to subscriber 1
[SUBSCRIBER: 1][ITEMS TO EMIT: 2,3]
[SUBSCRIBER: 2][ITEMS TO EMIT: 1,2,3]
emit "1" to subscriber 2
[SUBSCRIBER: 1][ITEMS TO EMIT: 2,3]
[SUBSCRIBER: 2][ITEMS TO EMIT: 2,3]
...

Bien que la commande ne soit pas garantie en raison de courses multi-thread.

2) publish et autoConnect ne s'annulent pas. Ils ajoutent seulement.

dataSource = ...;
dataSourceShared = dataSource.publish().autoConnect();

Désormais, lorsque vous abonnez plusieurs abonnés à dataSourceShared, cela n'entraîne qu'un seul abonnement à dataSource d'origine. C'est à dire. vous n'avez pas à émettre de nouvelle série de messages pour chaque nouvel abonné.

9
Yaroslav Stavnichiy