Quando a pesquisa de eventos seria melhor do que usar o padrão de observador?

41

Existem cenários em que a pesquisa de eventos seria melhor do que usar o padrão observador ? Tenho medo de usar a pesquisa e só começaria a usá-la se alguém me desse um bom cenário. Tudo o que consigo pensar é em como o padrão de observador é melhor do que nas pesquisas. Considere este cenário:

Você está programando um simulador de carro. O carro é um objeto. Assim que o carro ligar, você deseja reproduzir um clipe de som "vroom vroom".

Você pode modelar isso de duas maneiras:

Polling : Poll o objeto carro a cada segundo para ver se está ligado. Quando estiver ativado, reproduza o clipe de som.

Padrão de observador : Faça do carro o Assunto do padrão de observador. Faça com que ele publique o evento "on" para todos os observadores quando ele for ativado. Crie um novo objeto de som que ouça o carro. Faça com que implemente o retorno de chamada "ativado", que reproduz o clipe de som.

Nesse caso, acho que o padrão de observador vence. Em primeiro lugar, a pesquisa é mais intensiva do processador. Em segundo lugar, o clipe de som não dispara imediatamente quando o carro liga. Pode haver um intervalo de até 1 segundo por causa do período de votação.

JoJo
fonte
Não consigo pensar em quase nenhum cenário. Padrão de observador é o que realmente mapeia para o mundo real e a vida real. Portanto, acho que nenhum cenário jamais justificaria não usá-lo.
Saeed Neamati 22/08/11
Você está falando sobre eventos da interface do usuário ou eventos em geral?
Bryan Oakley
3
Seu exemplo não remove o problema de pesquisa / observação. Você simplesmente passou para um nível mais baixo. Seu programa ainda precisa descobrir se o carro está ligado ou não por algum mecanismo.
Dunk

Respostas:

55

Imagine que você deseja ser notificado sobre todos os ciclos do motor, por exemplo, para exibir uma medição de RPM para o motorista.

Padrão do observador: O mecanismo publica um evento de "ciclo do mecanismo" para todos os observadores em cada ciclo. Crie um ouvinte que conte eventos e atualiza a exibição do RPM.

Pesquisa: O visor de RPM solicita ao motor em intervalos regulares um contador de ciclo do motor e atualiza o visor de RPM de acordo.

Nesse caso, o padrão do observador provavelmente perderia: o ciclo do mecanismo é um processo de alta frequência e alta prioridade, você não deseja atrasar ou paralisar esse processo apenas para atualizar uma exibição. Você também não deseja debater o pool de threads com eventos do ciclo do mecanismo.


PS: Eu também uso o padrão de pesquisa frequentemente na programação distribuída:

Padrão do observador: O processo A envia uma mensagem ao processo B que diz "sempre que um evento E ocorrer, envie uma mensagem ao processo A".

Padrão de pesquisa: O processo A envia regularmente uma mensagem para o processo B, que diz "se o evento E ocorreu desde a última vez que fiz a pesquisa, envie-me uma mensagem agora".

O padrão de pesquisa produz um pouco mais de carga na rede. Mas o padrão do observador também tem desvantagens:

  • Se o processo A travar, ele nunca cancelará a inscrição e o processo B tentará enviar notificações a ele por toda a eternidade, a menos que possa detectar com segurança falhas de processo remoto (não é uma coisa fácil de fazer)
  • Se o evento E for muito frequente e / ou as notificações tiverem muitos dados, o processo A poderá receber mais notificações de eventos do que ele pode suportar. Com o padrão de polling, ele pode simplesmente acelerar o polling.
  • No padrão do observador, a alta carga pode causar "ondulações" em todo o sistema. Se você usar soquetes de bloqueio, essas ondulações podem ocorrer nos dois sentidos.
nikie
fonte
1
Bom ponto. Às vezes, é melhor também pesquisar por uma questão de desempenho.
Falcon
1
O número esperado de observadores também é uma consideração. Quando você espera um grande número de observadores, atualizá-los todos a partir do observado pode se tornar um gargalo de desempenho. Muito mais fácil, basta escrever um valor em algum lugar e fazer com que os "observadores" verifiquem esse valor quando necessário.
Marjan Venema
1
"a menos que possa detectar com segurança falhas de processo remoto (não é uma coisa fácil de fazer)" ... exceto por pesquisas; P. Portanto, o melhor design para minimizar a resposta "nada mudou" o máximo possível. +1, boa resposta.
Pd
2
@Jojo: Você poderia, sim, mas depois está colocando a política que deve pertencer à exibição no contador de RPM. Talvez o usuário ocasionalmente queira ter uma exibição de RPM altamente precisa.
Zan Lynx
2
@JoJo: Publicar cada 100º evento é um hack. Só funciona bem se a frequência do evento estiver sempre na faixa correta, se o processamento do evento não demorar muito para o mecanismo, se todos os assinantes precisarem de precisão comparável. E é necessária uma operação de módulo por RPM, que (assumindo alguns milhares de RPM) é muito mais trabalhosa para a CPU do que algumas operações de pesquisa por segundo.
Nikie
7

A pesquisa é melhor se o processo de pesquisa for consideravelmente mais lento do que o que é pesquisado. Se você estiver gravando eventos em um banco de dados, geralmente é melhor pesquisar todos os seus produtores de eventos, coletar todos os eventos que ocorreram desde a última pesquisa e gravá-los em uma única transação. Se você tentou gravar todos os eventos como eles ocorreram, talvez não consiga acompanhar e, eventualmente, terá problemas quando as filas de entrada forem preenchidas. Também faz mais sentido em sistemas distribuídos fracamente acoplados, onde a latência é alta ou a configuração e desmontagem de conexões são caras. Acho os sistemas de pesquisa mais fáceis de escrever e entender, mas na maioria das situações os observadores ou os consumidores orientados a eventos parecem oferecer melhor desempenho (na minha experiência).

TMN
fonte
7

A pesquisa é muito mais fácil de trabalhar em uma rede quando as conexões podem falhar, os servidores podem ser executados etc. Lembre-se de que no final do dia um soquete TCP precisa de mensagens de "polling" para manter a vida, caso contrário, o servidor assumirá o cliente Foi-se embora.

A pesquisa também é boa quando você deseja manter uma interface do usuário atualizada, mas os objetos subjacentes mudam muito rapidamente , não há sentido em atualizar a interface do usuário mais do que algumas vezes por segundo na maioria dos aplicativos.

Desde que o servidor possa responder "sem alteração" a um custo muito baixo e você não realiza pesquisas com muita frequência e não possui milhares de clientes pesquisando, a pesquisa funciona muito bem na vida real.

No entanto, para casos " na memória ", eu uso o padrão observador como padrão, pois normalmente é o menos trabalhoso.

Ian
fonte
5

A pesquisa tem algumas desvantagens, você basicamente as declarou na sua pergunta.

No entanto, pode ser uma solução melhor quando você deseja realmente desacoplar o observável de qualquer observador. Mas às vezes pode ser melhor usar um invólucro observável para que o objeto seja observado nesses casos.

Eu usaria a pesquisa apenas quando o observável não puder ser observado com interações com objetos, o que geralmente ocorre quando se consulta bancos de dados, por exemplo, onde você não pode ter retornos de chamada. Outro problema pode ser o multithreading, onde muitas vezes é mais seguro pesquisar e processar mensagens, em vez de chamar objetos diretamente, para evitar problemas de simultaneidade.

Falcão
fonte
Não sei bem por que você acha que a pesquisa é mais segura para multiencadeamento. Na maioria dos casos, esse não será o caso. Quando o manipulador de pesquisas recebe uma solicitação de pesquisa, ele precisa descobrir o estado do objeto pesquisado; se o objeto está no meio da atualização, não é seguro para o manipulador de pesquisas. No cenário do ouvinte, você só recebe notificações se o empurrador estiver em um estado consistente, para evitar a maioria dos problemas de sincronização no objeto pesquisado.
Lie Ryan
4

Para um bom exemplo de quando a pesquisa substitui a notificação, observe as pilhas de rede do sistema operacional.

Foi um grande negócio para o Linux quando a pilha de rede ativou a NAPI, uma API de rede que permitia aos drivers alternar do modo de interrupção (notificação) para o modo de pesquisa.

Com várias interfaces Ethernet de gigabit, as interrupções frequentemente sobrecarregam a CPU, fazendo com que o sistema funcione mais lentamente do que deveria. Com a pesquisa, as placas de rede coletam pacotes em buffers até serem pesquisadas ou até as placas gravam os pacotes na memória via DMA. Então, quando o sistema operacional estiver pronto, ele pesquisa todos os dados no cartão e realiza o processamento TCP / IP padrão.

O modo de pesquisa permite que a CPU colete dados Ethernet na sua taxa máxima de processamento sem carga de interrupção inútil. O modo de interrupção permite que a CPU fique ociosa entre pacotes quando o trabalho não está tão ocupado.

O segredo é quando mudar de um modo para outro. Cada modo tem vantagens e deve ser usado no local apropriado.

Zan Lynx
fonte
2

Eu amo votar! Eu? Sim! Eu? Sim! Eu? Sim! Eu ainda? Sim! E agora? Sim!

Como outros já mencionaram, pode ser incrivelmente ineficiente se você estiver pesquisando apenas para recuperar o mesmo estado inalterado repetidamente. Essa é uma receita para queimar os ciclos da CPU e reduzir significativamente a vida útil da bateria em dispositivos móveis. É claro que não é um desperdício se você está voltando a um estado novo e significativo toda vez, a uma velocidade não mais rápida do que o desejado.

Mas a principal razão pela qual eu amo a pesquisa é por causa de sua simplicidade e natureza previsível. Você pode rastrear o código e ver facilmente quando e onde as coisas vão acontecer e em qual thread. Se, teoricamente, vivêssemos em um mundo onde a pesquisa era um desperdício insignificante (embora a realidade esteja longe disso), então acredito que simplificaria a manutenção de um código tremendo. E esse é o benefício de pesquisar e puxar, como vejo se poderíamos desconsiderar o desempenho, mesmo que não devamos nesse caso.

Quando comecei a programar na era do DOS, meus joguinhos giravam em torno das pesquisas. Copiei algum código de montagem de um livro que eu mal entendia sobre interrupções no teclado e o fiz armazenar um buffer de estados do teclado, momento em que meu loop principal estava sempre pesquisando. A tecla para cima está pressionada? Não. A tecla para cima está pressionada? Não. Que tal agora? Não. Agora? Sim. Ok, mova o jogador.

E, embora incrivelmente inútil, achei muito mais fácil raciocinar em comparação com os dias de programação multitarefa e orientada a eventos. Eu sabia exatamente quando e onde as coisas iriam ocorrer o tempo todo e era mais fácil manter as taxas de quadros estáveis ​​e previsíveis sem soluços.

Então, desde então, eu sempre tentei encontrar uma maneira de obter alguns dos benefícios e previsibilidade disso sem realmente queimar os ciclos da CPU, como usar variáveis ​​de condição para notificar os threads a serem ativados, em que ponto eles podem obter o novo estado, faça o que quiserem e volte a dormir esperando ser notificado novamente.

E, de alguma maneira, acho as filas de eventos muito mais fáceis de trabalhar, pelo menos do que os padrões de observadores, mesmo que ainda não tornem tão fácil prever para onde você vai parar ou o que vai acontecer. Eles pelo menos centralizam o fluxo de controle de manipulação de eventos em algumas áreas principais do sistema e sempre manipulam esses eventos no mesmo encadeamento em vez de passar de uma função para um lugar completamente remoto e inesperado, de repente fora de um encadeamento central de manipulação de eventos. Portanto, a dicotomia nem sempre precisa ser entre observadores e pesquisas. As filas de eventos são uma espécie de meio termo.

Mas sim, de alguma forma, acho muito mais fácil raciocinar sobre sistemas que fazem coisas que são analogicamente mais próximas do tipo de fluxos de controle previsíveis que eu costumava ter quando estava pesquisando há séculos, enquanto contrariava a tendência do trabalho ocorrer em momentos em que nenhuma alteração de estado ocorreu. Portanto, há esse benefício se você puder fazê-lo de uma maneira que não esteja queimando os ciclos da CPU desnecessariamente como nas variáveis ​​de condição.

Loops homogêneos

Tudo bem, recebi um ótimo comentário Josh Caswellque apontou alguma bobagem na minha resposta:

"como usar variáveis ​​de condição para notificar os threads a serem ativados" Parece um arranjo baseado em evento / observador, não em polling

Tecnicamente, a própria variável de condição está aplicando o padrão de observador para ativar / notificar threads, portanto, chamar isso de "polling" provavelmente seria incrivelmente enganador. Mas acho que ele fornece um benefício semelhante ao encontrado nos dias do DOS (apenas em termos de fluxo de controle e previsibilidade). Vou tentar explicar melhor.

O que eu achei atraente naqueles dias era que você podia olhar para uma seção do código ou segui-la e dizer: "Ok, toda esta seção é dedicada à manipulação de eventos do teclado. Nada mais acontecerá nesta seção do código E eu sei exatamente o que vai acontecer antes, e sei exatamente o que vai acontecer depois (física e renderização, por exemplo). " A pesquisa dos estados do teclado deu a você esse tipo de centralização do fluxo de controle na medida em que lida com o que deve continuar em resposta a esse evento externo. Não respondemos a esse evento externo imediatamente. Respondemos a ele conforme nossa conveniência.

Quando usamos um sistema push baseado em um padrão Observer, geralmente perdemos esses benefícios. Um controle pode ser redimensionado, o que aciona um evento de redimensionamento. Quando o rastreamos, descobrimos que estamos dentro de um controle exótico, que faz muitas coisas personalizadas no redimensionamento, o que desencadeia mais eventos. Acabamos sendo completamente surpresos ao rastrear todos esses eventos em cascata sobre onde terminamos no sistema. Além disso, podemos descobrir que tudo isso nem sempre ocorre em um determinado encadeamento, porque o encadeamento A pode redimensionar um controle aqui, enquanto o encadeamento B também redimensiona um controle posteriormente. Por isso, sempre achei isso muito difícil de argumentar, dado o quão difícil é prever onde tudo acontece e o que acontecerá.

A fila de eventos é um pouco mais simples para eu pensar, porque simplifica onde todas essas coisas acontecem pelo menos no nível do encadeamento. No entanto, muitas coisas díspares podem estar acontecendo. Uma fila de eventos pode conter uma mistura eclética de eventos a serem processados, e cada um ainda pode nos surpreender quanto à cascata de eventos que ocorreram, a ordem em que foram processados ​​e como acabamos saltando por todo o lugar na base de código .

O que considero "o mais próximo" da pesquisa não usaria uma fila de eventos, mas adiaria um tipo de processamento muito homogêneo. Um PaintSystempode ser alertado por meio de uma variável de condição que há trabalho de pintura para repintar determinadas células da grade de uma janela; nesse ponto, ele faz um loop seqüencial simples através das células e repinta tudo dentro dela na ordem z adequada. Pode haver um nível de chamada indireta / despacho dinâmico aqui para acionar os eventos de pintura em cada widget que residem em uma célula que precisa ser redesenhada, mas é isso - apenas uma camada de chamadas indiretas. A variável de condição usa o padrão observador para alertar o PaintSystemque ele tem trabalho a fazer, mas não especifica nada além disso, e oPaintSystemé dedicado a uma tarefa uniforme e muito homogênea nesse ponto. Quando estamos depurando e rastreando o PaintSystem'scódigo, sabemos que nada mais acontecerá, exceto a pintura.

Portanto, trata-se principalmente de levar o sistema ao ponto em que essas coisas executam loops homogêneos sobre dados, aplicando uma responsabilidade muito singular sobre eles, em vez de loops não homogêneos sobre tipos diferentes de dados que executam inúmeras responsabilidades, como podemos obter no processamento da fila de eventos.

Nosso objetivo é esse tipo de coisa:

when there's work to do:
   for each thing:
       apply a very specific and uniform operation to the thing

Ao contrário de:

when one specific event happens:
    do something with relevant thing
in relevant thing's event:
    do some more things
in thing1's triggered by thing's event:
    do some more things
in thing2's event triggerd by thing's event:
    do some more things:
in thing3's event triggered by thing2's event:
    do some more things
in thing4's event triggered by thing1's event:
    cause a side effect which shouldn't be happening
    in this order or from this thread.

E assim por diante. E não precisa ser um thread por tarefa. Um encadeamento pode aplicar lógica de layouts (redimensionamento / reposicionamento) aos controles da GUI e repintá-los, mas pode não manipular cliques no teclado ou no mouse. Portanto, você pode considerar isso apenas melhorando a homogeneidade de uma fila de eventos. Mas não precisamos usar uma fila de eventos e intercalar as funções de redimensionamento e pintura. Podemos fazer como:

in thread dedicated to layout and painting:
    when there's work to do:
         for each widget that needs resizing/reposition:
              resize/reposition thing to target size/position
              mark appropriate grid cells as needing repainting
         for each grid cell that needs repainting:
              repaint cell
         go back to sleep

Portanto, a abordagem acima usa apenas uma variável de condição para notificar o encadeamento quando há trabalho a ser feito, mas não intercala tipos diferentes de eventos (redimensionar em um loop, pintar em outro loop, não uma mistura de ambos) e não ' não se preocupe em comunicar exatamente o que é o trabalho que precisa ser feito (o segmento "descobre" isso ao acordar olhando os estados do ECS em todo o sistema). Cada loop que ele executa é então muito homogêneo por natureza, facilitando o raciocínio sobre a ordem em que tudo acontece.

Não sei ao certo como chamar esse tipo de abordagem. Eu não vi outros motores de interface gráfica fazendo isso e é uma espécie de minha própria abordagem exótica à minha. Mas antes, quando tentei implementar estruturas de GUI multithread usando observadores ou filas de eventos, tive uma tremenda dificuldade em depurá-lo e também encontrei algumas condições obscuras de corrida e impasses que não era inteligente o suficiente para corrigir de uma maneira que me fez sentir confiante sobre a solução (algumas pessoas podem fazer isso, mas eu não sou inteligente o suficiente). Meu primeiro design de iteração chamou um slot diretamente através de um sinal e alguns slots geraram outros threads para fazer um trabalho assíncrono, e isso foi o mais difícil de se pensar e eu estava tropeçando nas condições de corrida e em impasses. A segunda iteração usou uma fila de eventos e isso foi um pouco mais fácil de raciocinar, mas não é fácil o suficiente para o meu cérebro fazê-lo sem ainda correr para o obscuro impasse e a condição racial. A terceira e a iteração final usaram a abordagem descrita acima e, finalmente, isso me permitiu criar uma estrutura de GUI multithread que até mesmo um simplório idiota como eu poderia implementar corretamente.

Então esse tipo de design final de GUI multithread me permitiu criar outra coisa que era muito mais fácil de raciocinar e evitar os tipos de erros que eu costumava cometer, e um dos motivos pelos quais achei muito mais fácil raciocinar em o mínimo é por causa desses loops homogêneos e como eles se pareciam com o fluxo de controle semelhante a quando eu estava pesquisando nos dias do DOS (mesmo que não esteja realmente pesquisando e apenas executando trabalho quando há trabalho a ser feito). A idéia era se afastar o máximo possível do modelo de manipulação de eventos, o que implica loops não homogêneos, efeitos colaterais não homogêneos, fluxos de controle não homogêneos e trabalhar cada vez mais para loops homogêneos operando uniformemente em dados homogêneos e isolando e unificar os efeitos colaterais de maneira a facilitar o foco no "quê"


fonte
1
"como usar variáveis ​​de condição para notificar threads a serem ativadas" Parece um arranjo baseado em evento / observador, não em pesquisa.
Josh Caswell
Acho a diferença muito sutil, mas a notificação é apenas na forma de "Há trabalho a ser feito" para ativar os threads. Como exemplo, um padrão de observador pode, ao redimensionar um controle pai, redimensionar em cascata as chamadas de chamadas na hierarquia usando o despacho dinâmico. As coisas teriam suas funções de eventos de redimensionamento chamadas indiretamente imediatamente. Então eles podem se pintar imediatamente. Então, se nós usamos uma fila de eventos, redimensionar um controle pai pode empurrar eventos de redimensionamento para baixo na hierarquia, altura em que as funções de redimensionamento para cada controle pode ter chamado de forma diferida, em que ...
... apontam que eles podem enviar eventos de repintura que, da mesma forma, são chamados de maneira adiada depois que tudo é redimensionado e tudo a partir de um segmento central de manipulação de eventos. E acho que a centralização é benéfica, pelo menos no que diz respeito à depuração e capaz de raciocinar facilmente sobre onde o processamento está ocorrendo (incluindo qual thread) ... Então, o que considero o mais próximo da pesquisa não é essas soluções ...
Seria, por exemplo, um LayoutSystemque normalmente está em suspensão, mas quando o usuário redimensiona um controle, ele usaria uma variável de condição para ativar o LayoutSystem. Em seguida, o LayoutSystemredimensiona todos os controles necessários e volta a dormir. No processo, as regiões retangulares em que os widgets residem são marcadas como necessitando de atualizações; nesse momento, um é PaintSystemativado e passa por essas regiões retangulares, repintando as que precisam ser redesenhadas em um loop seqüencial plano.
Portanto, a própria variável de condição segue um padrão de observador para notificar os threads a serem ativados, mas não estamos transmitindo mais informações do que "há trabalho a ser feito". E cada sistema que acorda é dedicado a processar as coisas em um loop muito simples, aplicando uma tarefa muito homogênea, em oposição a uma fila de eventos que possui tarefas não homogêneas (pode conter uma mistura eclética de eventos a serem processados).
-4

Estou dando uma visão geral mais sobre o modo conceitual de pensar sobre o padrão do observador. Pense em um cenário como se inscrever em canais do youtube. Há um número de usuários que se inscrevem no canal e, quando houver alguma atualização no canal que inclua muitos vídeos, o assinante será notificado de que há uma alteração nesse canal específico. Concluímos que, se o canal é ASSUNTO e tem a capacidade de se inscrever, cancele a inscrição e notifique todos os OBSERVADORs registrados no canal.

Aroop Bhattacharya
fonte
2
isso nem tenta abordar a pergunta, quando a pesquisa de eventos seria melhor do que usar o padrão de observador. Veja como responder
gnat