Agendadores em RxJava

Agendadores em RxJava

*1. Visão geral *

Neste artigo, focaremos nos diferentes tipos de Schedulers que vamos usar para escrever programas multithreading baseados nos métodos subscribeOn_ e observeOn do RxJava _Observable.

Schedulers oferecem a oportunidade de especificar onde e provavelmente quando executar tarefas relacionadas à operação de uma cadeia Observable.

Podemos obter um Scheduler dos métodos de fábrica descritos na classe http://reactivex.io/RxJava/javadoc/rx/schedulers/Schedulers.html [Schedulers] .

===* 2. Comportamento padrão de encadeamento *

*Por padrão,*  *Rx é de thread único* , o que implica que um _Observable_ e a cadeia de operadores que podemos aplicar a ele notificarão seus observadores no mesmo thread no qual seu método _subscribe () _ é chamado.

Os métodos observeOn e subscribeOn tomam como argumento um _Scheduler, _ que, como o nome sugere, é uma ferramenta que podemos usar para agendar ações individuais.

Criaremos nossa implementação de um Scheduler usando o método createWorker, que retorna um http://reactivex.io/RxJava/javadoc/rx/Scheduler.Worker.html [Scheduler.Worker] . Um worker aceita ações e executa sequencialmente em um único encadeamento.

De certa forma, um trabalhador _ é um agendador S, mas não iremos nos referir a ele como agendador para evitar confusão.

2.1 Agendando uma ação

Podemos agendar um trabalho em qualquer Scheduler criando um novo worker e agendando algumas ações:

Scheduler scheduler = Schedulers.immediate();
Scheduler.Worker worker = scheduler.createWorker();
worker.schedule(() -> result += "action");

Assert.assertTrue(result.equals("action"));

A ação é enfileirada no encadeamento ao qual o trabalhador está atribuído.

2.2 Cancelando uma ação

Scheduler.Worker estende Subscription. A chamada do método unsubscribe em um worker resultará no esvaziamento da fila e no cancelamento de todas as tarefas pendentes. Podemos ver isso por exemplo:

Scheduler scheduler = Schedulers.newThread();
Scheduler.Worker worker = scheduler.createWorker();
worker.schedule(() -> {
    result += "First_Action";
    worker.unsubscribe();
});
worker.schedule(() -> result += "Second_Action");

Assert.assertTrue(result.equals("First_Action"));

A segunda tarefa nunca é executada porque a anterior cancelou toda a operação. As ações que estavam em processo de execução serão interrompidas.

*3. Schedulers.newThread *

Esse agendador simplesmente inicia um novo encadeamento sempre que solicitado por _subscribeOn () _ ou _observeOn () _.

Quase nunca é uma boa escolha, não apenas por causa da latência envolvida ao iniciar um encadeamento, mas também porque esse encadeamento não é reutilizado:

Observable.just("Hello")
  .observeOn(Schedulers.newThread())
  .doOnNext(s ->
    result2 += Thread.currentThread().getName()
  )
  .observeOn(Schedulers.newThread())
  .subscribe(s ->
    result1 += Thread.currentThread().getName()
  );
Thread.sleep(500);
Assert.assertTrue(result1.equals("RxNewThreadScheduler-1"));
Assert.assertTrue(result2.equals("RxNewThreadScheduler-2"));

Quando o Worker é concluído, o thread simplesmente termina. Esse Scheduler pode ser usado apenas quando as tarefas são de granulação grossa: leva muito tempo para ser concluído, mas existem muito poucos deles para que os segmentos dificilmente sejam reutilizados.

Scheduler scheduler = Schedulers.newThread();
Scheduler.Worker worker = scheduler.createWorker();
worker.schedule(() -> {
    result += Thread.currentThread().getName() + "_Start";
    worker.schedule(() -> result += "_worker_");
    result += "_End";
});
Thread.sleep(3000);
Assert.assertTrue(result.equals(
  "RxNewThreadScheduler-1_Start_End_worker_"));

Quando agendamos o worker em um _NewThreadScheduler, _ vimos que o trabalhador estava vinculado a um thread específico.

===* 4. Schedulers.immediate *

Schedulers.immediate é um planejador especial que chama uma tarefa no encadeamento do cliente de maneira bloqueadora, em vez de assincronamente, e retorna quando a ação é concluída:

Scheduler scheduler = Schedulers.immediate();
Scheduler.Worker worker = scheduler.createWorker();
worker.schedule(() -> {
    result += Thread.currentThread().getName() + "_Start";
    worker.schedule(() -> result += "_worker_");
    result += "_End";
});
Thread.sleep(500);
Assert.assertTrue(result.equals(
  "main_Start_worker__End"));

De fato, assinar um Observable via immediate Scheduler normalmente tem o mesmo efeito que não assinar um Scheduler em particular:

Observable.just("Hello")
  .subscribeOn(Schedulers.immediate())
  .subscribe(s ->
    result += Thread.currentThread().getName()
  );
Thread.sleep(500);
Assert.assertTrue(result.equals("main"));

===* 5. Schedulers.trampoline *

O trampoline Scheduler é muito semelhante ao immediate porque também agenda tarefas no mesmo encadeamento, bloqueando efetivamente.

No entanto, a tarefa a seguir é executada quando todas as tarefas agendadas anteriormente são concluídas: [.annotator-hl .annotator-hl-temporary]

Observable.just(2, 4, 6, 8)
  .subscribeOn(Schedulers.trampoline())
  .subscribe(i -> result += "" + i);
Observable.just(1, 3, 5, 7, 9)
  .subscribeOn(Schedulers.trampoline())
  .subscribe(i -> result += "" + i);
Thread.sleep(500);
Assert.assertTrue(result.equals("246813579"));

Immediate chama imediatamente uma tarefa, enquanto trampoline aguarda a conclusão da tarefa atual.

O trampoline‘s worker executa todas as tarefas no encadeamento que agendou a primeira tarefa. A primeira chamada para schedule está bloqueando até que a fila seja esvaziada:

Scheduler scheduler = Schedulers.trampoline();
Scheduler.Worker worker = scheduler.createWorker();
worker.schedule(() -> {
    result += Thread.currentThread().getName() + "Start";
    worker.schedule(() -> {
        result += "_middleStart";
        worker.schedule(() ->
            result += "_worker_"
        );
        result += "_middleEnd";
    });
    result += "_mainEnd";
});
Thread.sleep(500);
Assert.assertTrue(result
  .equals("mainStart_mainEnd_middleStart_middleEnd_worker_"));

===* 6. Schedulers.from *

Schedulers são internamente mais complexos que Executors de java.util.concurrent - portanto, era necessária uma abstração separada.

Mas como eles são conceitualmente bastante semelhantes, não surpreende que exista um wrapper que possa transformar Executor em Scheduler usando o método from factory:

private ThreadFactory threadFactory(String pattern) {
    return new ThreadFactoryBuilder()
      .setNameFormat(pattern)
      .build();
}

@Test
public void givenExecutors_whenSchedulerFrom_thenReturnElements()
 throws InterruptedException {

    ExecutorService poolA = newFixedThreadPool(
      10, threadFactory("Sched-A-%d"));
    Scheduler schedulerA = Schedulers.from(poolA);
    ExecutorService poolB = newFixedThreadPool(
      10, threadFactory("Sched-B-%d"));
    Scheduler schedulerB = Schedulers.from(poolB);

    Observable<String> observable = Observable.create(subscriber -> {
      subscriber.onNext("Alfa");
      subscriber.onNext("Beta");
      subscriber.onCompleted();
    });;

    observable
      .subscribeOn(schedulerA)
      .subscribeOn(schedulerB)
      .subscribe(
        x -> result += Thread.currentThread().getName() + x + "_",
        Throwable::printStackTrace,
        () -> result += "_Completed"
      );
    Thread.sleep(2000);
    Assert.assertTrue(result.equals(
      "Sched-A-0Alfa_Sched-A-0Beta__Completed"));
}

SchedulerB é usado por um curto período de tempo, mas mal agenda uma nova ação no schedulerA, que faz todo o trabalho. Portanto, vários métodos subscribeOn não são apenas ignorados, mas também introduzem uma pequena sobrecarga.

===* 7. Schedulers.io *

Esse Scheduler é semelhante ao newThread, exceto pelo fato de que os threads já iniciados são reciclados e podem manipular solicitações futuras.

Essa implementação funciona de maneira semelhante a ThreadPoolExecutor de java.util.concurrent com um conjunto ilimitado de threads. Toda vez que um novo worker é solicitado, um novo thread é iniciado (e posteriormente mantido ocioso por algum tempo) ou o ocioso é reutilizado:

Observable.just("io")
  .subscribeOn(Schedulers.io())
  .subscribe(i -> result += Thread.currentThread().getName());

Assert.assertTrue(result.equals("RxIoScheduler-2"));

Precisamos ter cuidado com recursos ilimitados de qualquer tipo - no caso de dependências externas lentas ou sem resposta, como serviços da web, io scheduler pode iniciar um enorme número de threads, fazendo com que nosso próprio aplicativo fique sem resposta.

Na prática, seguir Schedulers.io é quase sempre uma escolha melhor.

===* 8. Schedulers.computation *

Computation Scheduler, por padrão, limita o número de threads em execução paralelamente ao valor de _availableProcessors () _, conforme encontrado na classe de utilitário _Runtime.getRuntime () _.

Portanto, devemos usar um computation scheduler quando as tarefas são totalmente ligadas à CPU; isto é, eles exigem energia computacional e não possuem código de bloqueio.

Ele usa uma fila ilimitada na frente de cada encadeamento; portanto, se a tarefa for agendada, mas todos os núcleos estiverem ocupados, ela ficará na fila. No entanto, a fila imediatamente antes de cada encadeamento continuará crescendo:

Observable.just("computation")
  .subscribeOn(Schedulers.computation())
  .subscribe(i -> result += Thread.currentThread().getName());
Assert.assertTrue(result.equals("RxComputationScheduler-1"));

Se, por algum motivo, precisarmos de um número diferente de threads que o padrão, sempre podemos usar a propriedade rx.scheduler.max-computation-threads system.

Ao usar menos threads, podemos garantir que sempre haja um ou mais núcleos da CPU ociosos e, mesmo sob carga pesada, o pool de threads computation não sature o servidor. Simplesmente não é possível ter mais threads de computação que núcleos.

===* 9. Schedulers.test *

Esse Scheduler é usado apenas para fins de teste e nunca o veremos no código de produção. Sua principal vantagem é a capacidade de avançar o relógio, simulando o tempo passando arbitrariamente:

List<String> letters = Arrays.asList("A", "B", "C");
TestScheduler scheduler = Schedulers.test();
TestSubscriber<String> subscriber = new TestSubscriber<>();

Observable<Long> tick = Observable
  .interval(1, TimeUnit.SECONDS, scheduler);

Observable.from(letters)
  .zipWith(tick, (string, index) -> index + "-" + string)
  .subscribeOn(scheduler)
  .subscribe(subscriber);

subscriber.assertNoValues();
subscriber.assertNotCompleted();

scheduler.advanceTimeBy(1, TimeUnit.SECONDS);
subscriber.assertNoErrors();
subscriber.assertValueCount(1);
subscriber.assertValues("0-A");

scheduler.advanceTimeTo(3, TimeUnit.SECONDS);
subscriber.assertCompleted();
subscriber.assertNoErrors();
subscriber.assertValueCount(3);
assertThat(
  subscriber.getOnNextEvents(),
  hasItems("0-A", "1-B", "2-C"));

===* 10. Agendadores padrão *

Alguns operadores Observable no RxJava possuem formas alternativas que permitem definir qual Scheduler o operador usará para sua operação. Outros não operam em nenhum Scheduler específico ou operam em um Scheduler padrão específico.

Por exemplo, o operador delay pega os eventos upstream e os empurra downstream depois de um determinado tempo. Obviamente, ele não pode conter o encadeamento original durante esse período, portanto, ele deve usar um Scheduler diferente:

ExecutorService poolA = newFixedThreadPool(
  10, threadFactory("Sched1-"));
Scheduler schedulerA = Schedulers.from(poolA);
Observable.just('A', 'B')
  .delay(1, TimeUnit.SECONDS, schedulerA)
  .subscribe(i -> result+= Thread.currentThread().getName() + i + " ");

Thread.sleep(2000);
Assert.assertTrue(result.equals("Sched1-A Sched1-B "));

Sem fornecer um schedulerA personalizado, todos os operadores abaixo de delay usariam o computation Scheduler.

Outros operadores importantes que suportam Schedulers personalizados são buffer, _interval, range, timer, skip, take, timeout e vários outros. Se não fornecermos um Scheduler a esses operadores, o computation scheduler será utilizado, o que é um padrão seguro na maioria dos casos.

===* 11. Conclusão*

Em aplicativos verdadeiramente reativos, para os quais todas as operações de execução longa são assíncronas, são necessários muito poucos encadeamentos e, portanto, Schedulers.

Os planejadores de masterização são essenciais para escrever código escalável e seguro usando o RxJava. A diferença entre subscribeOn e observeOn é especialmente importante sob carga alta, onde todas as tarefas devem ser executadas exatamente quando esperamos.

Por último, mas não menos importante, devemos ter certeza de que os Schedulers usados ​​a jusante podem acompanhar o lo [.annotator-hl .annotator-hl-temporary] anúncio gerado por Schedulers [. Annotator-hl .annotator-hl-temporary ] upstrea m. Para obter mais informações, existe este artigo sobre o link:/rxjava-backpressure [backpressure].

A implementação de todos esses exemplos e trechos de código pode ser encontrada no GitHub project - este é um projeto do Maven, portanto, deve ser fácil importar e executar como está.