Diferença entre Consumidor / Produtor e Observador / Observável

15

Estou trabalhando no design de um aplicativo que consiste em três partes:

  • um único encadeamento que observa a ocorrência de certos eventos (criação de arquivo, solicitações externas etc.)
  • N threads de trabalho que respondem a esses eventos processando-os (cada trabalhador processa e consome um único evento e o processamento pode levar tempo variável)
  • um controlador que gerencia esses encadeamentos e manipula erros (reinicialização de encadeamentos, registro de resultados)

Embora isso seja bastante básico e não seja difícil de implementar, estou me perguntando qual seria a maneira "correta" de fazer isso (neste caso concreto em Java, mas também são apreciadas respostas de abstração mais altas). Duas estratégias vêm à mente:

  • Observador / Observável: O segmento de observação é observado pelo controlador. No caso de um evento acontecer, o controlador é notificado e pode atribuir a nova tarefa a um encadeamento livre a partir de um conjunto de encadeamentos em cache reutilizável (ou aguardar e armazenar em cache as tarefas na fila FIFO se todos os encadeamentos estiverem ocupados). Os threads de trabalho implementam Callable e retornam com êxito o resultado (ou um valor booleano) ou retornam com um erro; nesse caso, o controlador pode decidir o que fazer (dependendo da natureza do erro que ocorreu).

  • Produtor / consumidor : o thread de observação compartilha um BlockingQueue com o controlador (fila de eventos) e o controlador compartilha dois com todos os trabalhadores (fila de tarefas e fila de resultados). No caso de um evento, o thread de observação coloca um objeto de tarefa na fila de eventos. O controlador pega novas tarefas da fila de eventos, as revisa e as coloca na fila de tarefas. Cada trabalhador aguarda novas tarefas e as leva / consome da fila de tarefas (primeiro a ser atendido, gerenciado pela própria fila), colocando os resultados ou erros de volta na fila de resultados. Finalmente, o controlador pode recuperar os resultados da fila de resultados e executar as etapas necessárias em caso de erros.

Os resultados finais de ambas as abordagens são semelhantes, mas cada uma tem pequenas diferenças:

Com os Observadores, o controle de threads é direto e cada tarefa é atribuída a um novo trabalhador gerado específico. A sobrecarga para criação de threads pode ser maior, mas não muito graças ao conjunto de threads em cache. Por outro lado, o padrão Observer é reduzido para um único Observer em vez de múltiplo, o que não é exatamente para o qual foi projetado.

A estratégia de fila parece ser mais fácil de estender, por exemplo, adicionar vários produtores em vez de um é direto e não requer nenhuma alteração. A desvantagem é que todos os encadeamentos seriam executados indefinidamente, mesmo quando não estiver fazendo nenhum trabalho, e o tratamento de erros / resultados não parece tão elegante quanto na primeira solução.

Qual seria a abordagem mais adequada nessa situação e por quê? Achei difícil encontrar respostas para essa pergunta on-line, porque a maioria dos exemplos trata apenas de casos claros, como atualizar muitas janelas com um novo valor no caso Observer ou processar com vários consumidores e produtores. Qualquer entrada é muito apreciada.

user183536
fonte

Respostas:

10

Você está bem perto de responder sua própria pergunta. :)

No padrão Observable / Observer (observe o flip), há três coisas a serem lembradas:

  1. Geralmente, a notificação da alteração, ou seja, 'carga útil', é observável.
  2. O observável existe .
  3. Os observadores devem ser conhecidos pelo observável existente (ou então eles não têm nada para observar).

Ao combinar esses pontos, o que está implícito é que o observável sabe quais são seus componentes a jusante, ou seja, os observadores. O fluxo de dados é inerentemente derivado do observável - os observadores meramente 'vivem e morrem' pelo que estão observando.

No padrão Produtor / Consumidor, você obtém uma interação muito diferente:

  1. Geralmente, a carga útil existe independentemente do produtor responsável por produzi-la.
  2. Os produtores não sabem como ou quando consumidores estão ativos.
  3. Os consumidores não precisam conhecer o produtor da carga útil.

O fluxo de dados agora é completamente separado entre um produtor e um consumidor - tudo o que o produtor sabe é que possui uma saída e tudo que o consumidor sabe é que possui uma entrada. É importante ressaltar que isso significa que produtores e consumidores podem existir inteiramente sem a presença do outro.

Outra diferença não tão sutil é que vários observadores no mesmo observável geralmente recebem a mesma carga (a menos que exista uma implementação não convencional), enquanto que vários consumidores do mesmo produtor podem não ter. Isso depende se o intermediário for uma abordagem semelhante a uma fila ou a um tópico. O primeiro transmite uma mensagem diferente para cada consumidor, enquanto o segundo garante (ou tenta) que todos os consumidores processem em uma base por mensagem.

Para ajustá-los ao seu aplicativo:

  • No padrão Observable / Observer, sempre que seu thread de observação estiver inicializando, ele deve saber como informar o controlador. Como observador, o controlador provavelmente está aguardando uma notificação do thread de observação antes de permitir que os threads lidem com a alteração.
  • No padrão Produtor / Consumidor, seu encadeamento de observação precisa conhecer apenas a presença da fila de eventos e interagir apenas com isso. Como consumidor, o controlador pesquisa a fila de eventos e, depois de receber uma nova carga útil, permite que os encadeamentos o tratem.

Portanto, para responder sua pergunta diretamente: se você deseja manter algum nível de separação entre o encadeamento de observação e o controlador, para poder operá-los de forma independente, deve tender para o padrão Produtor / Consumidor.

hjk
fonte
2
Obrigado pela sua resposta detalhada. Infelizmente, não posso votar novamente por falta de reputação; por isso, marquei-a como uma solução. A independência temporal entre as duas partes que você mencionou é algo positivo que não pensei até agora. As filas podem gerenciar rajadas curtas de muitos eventos com longas pausas entre muito melhores do que a ação direta após os eventos serem observados (se a contagem máxima de encadeamentos for fixada e relativamente baixa). A contagem de encadeamentos também pode ser aumentada / diminuída dinamicamente, dependendo da contagem atual de itens da fila.
user183536
@ user183536 Sem problemas, prazer em ajudar! :)
hjk