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 Caswell
que 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 PaintSystem
pode 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 PaintSystem
que 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's
có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ê"