Por que é responsabilidade do chamador garantir a segurança do thread na programação da GUI?

37

Eu já vi, em muitos lugares, que é uma sabedoria canônica 1 que é responsabilidade do chamador garantir que você esteja no encadeamento da interface do usuário ao atualizar os componentes da interface do usuário (especificamente, no Java Swing, que você está no encadeamento de distribuição de eventos ) .

Porque isto é assim? O Event Dispatch Thread é uma preocupação da visualização no MVC / MVP / MVVM; para manipulá-lo em qualquer lugar, mas a visualização cria um forte acoplamento entre a implementação da visualização e o modelo de encadeamento da implementação dessa visualização.

Especificamente, digamos que eu tenho um aplicativo arquitetado MVC que usa o Swing. Se o chamador for responsável por atualizar os componentes no thread de distribuição de eventos, se eu tentar trocar minha implementação do Swing View por uma implementação JavaFX, devo alterar todo o código do apresentador / controlador para usar o thread do aplicativo JavaFX .

Então, suponho que tenho duas perguntas:

  1. Por que é responsabilidade do chamador garantir a segurança do encadeamento do componente da interface do usuário? Onde está a falha no meu raciocínio acima?
  2. Como posso projetar meu aplicativo para ter um acoplamento frouxo dessas preocupações de segurança de rosca, e ainda assim ser adequadamente seguro de rosca?

Deixe-me adicionar um código Java do MCVE para ilustrar o que quero dizer com "responsável pela chamada" (existem outras boas práticas aqui que não estou praticando, mas estou tentando, de propósito, ser o mínimo possível):

Chamador sendo responsável:

public class Presenter {
  private final View;

  void updateViewWithNewData(final Data data) {
    EventQueue.invokeLater(new Runnable() {
      public void run() {
        view.setData(data);
      }
    });
  }
}
public class View {
  void setData(Data data) {
    component.setText(data.getMessage());
  }
}

Ver ser responsável:

public class Presenter {
  private final View;

  void updateViewWithNewData(final Data data) {
    view.setData(data);
  }
}
public class View {
  void setData(Data data) {
    EventQueue.invokeLater(new Runnable() {
      public void run() {
        component.setText(data.getMessage());
      }
    });
  }
}

1: O autor dessa postagem tem a maior pontuação de tag no Swing no estouro de pilha. Ele diz isso em todo o lugar e também vi que é responsabilidade do interlocutor em outros lugares.

durron597
fonte
11
Eh, desempenho IMHO. Essas publicações de eventos não são gratuitas e, em aplicativos não triviais, você deseja minimizar o número (e garantir que nenhum seja muito grande), mas essa minimização / condensação deve ser feita logicamente no apresentador.
Ordous
11
@Ordous Você ainda pode garantir que os lançamentos sejam mínimos enquanto coloca a transferência de linha na exibição.
precisa saber é o seguinte
2
Há algum tempo, li um blog muito bom que discute esse problema, o que basicamente diz é que é muito perigoso tentar tornar o thread do kit de interface do usuário seguro, pois apresenta possíveis bloqueios e, dependendo de como ele é implementado, as condições de corrida no estrutura. Há também uma consideração de desempenho. Agora não muito, mas quando o Swing foi lançado, ele foi fortemente criticado por seu desempenho (ruim), mas isso não foi culpa do Swing, era falta de conhecimento das pessoas sobre como usá-lo.
MadProgrammer 02/09/2015
11
O SWT aplica o conceito de segurança de encadeamento lançando exceções se você violá-lo, não é bonito, mas pelo menos você está ciente disso. Você mencionou a mudança de Swing para JavaFX, mas você teria esse problema com praticamente qualquer estrutura de interface do usuário; o Swing parece ser o único que destaca o problema. Você pode projetar uma camada intermediária (um controlador de um controlador?) Cuja tarefa é garantir que as chamadas para a interface do usuário sejam sincronizadas corretamente. É impossível saber exatamente como você pode projetar suas peças não-UI da sua API a partir da perspectiva da API UI
MadProgrammer
11
e a maioria dos desenvolvedores reclamaria que qualquer proteção de thread implementada na API da interface do usuário fosse restritiva ou não atendesse às suas necessidades. Melhor para permitir que você decidir como você deseja resolver esta questão, com base em suas necessidades
MadProgrammer

Respostas:

22

No final de seu ensaio de sonho fracassado , Graham Hamilton (um grande arquiteto Java) menciona se os desenvolvedores "devem preservar a equivalência com um modelo de fila de eventos, eles precisam seguir várias regras não óbvias" e ter uma visibilidade visível e explícita. o modelo de fila de eventos "parece ajudar as pessoas a seguir o modelo com mais confiabilidade e, assim, construir programas GUI que funcionam de maneira confiável".

Em outras palavras, se você tentar colocar uma fachada multithread no topo de um modelo de fila de eventos, a abstração ocasionalmente vazará de maneiras não óbvias e extremamente difíceis de depurar. Parece que vai funcionar no papel, mas acaba caindo aos pedaços na produção.

Adicionar pequenos wrappers em torno de componentes únicos provavelmente não será problemático, como atualizar uma barra de progresso de um thread de trabalho. Se você tentar fazer algo mais complexo que exija vários bloqueios, ficará muito difícil entender como a camada multithread e a camada da fila de eventos interagem.

Observe que esses tipos de problemas são universais em todos os kits de ferramentas da GUI. Presumir que um modelo de despacho de evento no seu apresentador / controlador não o acopla firmemente a apenas um modelo de simultaneidade de um kit de ferramentas da GUI específico, ele o acopla a todos eles . A interface da fila de eventos não deve ser tão difícil de abstrair.

Karl Bielefeldt
fonte
25

Porque tornar o thread lib da GUI seguro é uma dor de cabeça enorme e um gargalo.

O fluxo de controle nas GUIs geralmente segue duas direções da fila de eventos para a janela raiz, para os widgets da GUI e do código do aplicativo para o widget propagado até a janela raiz.

É difícil conceber uma estratégia de bloqueio que não bloqueie a janela raiz (causará muita contenção) . Travar de baixo para cima enquanto outro segmento estiver travando de cima para baixo é uma ótima maneira de obter um impasse instantâneo.

E verificar se o encadeamento atual é o encadeamento da GUI sempre que custa caro e pode causar confusão sobre o que realmente está acontecendo com a GUI, especialmente quando você está executando uma sequência de gravação de atualização de leitura. Isso precisa ter um bloqueio nos dados para evitar corridas.

catraca arrepiante
fonte
11
Curiosamente, eu resolvo esse problema tendo uma estrutura de enfileiramento para essas atualizações que escrevi que gerencia a transferência de encadeamentos.
precisa saber é o seguinte
2
@ durron597: E você nunca tem atualizações dependendo do estado atual da interface do usuário que outro segmento possa influenciar? Então pode funcionar.
Deduplicator
Por que você precisaria de bloqueios aninhados? Por que você precisa bloquear a janela raiz inteira ao trabalhar em detalhes em uma janela filho? Fim Lock é um problema solucionável, fornecendo um único método exposto ao fechamento fechamentos mutliple na ordem certa (ou de cima para baixo ou de baixo para cima, mas não deixar a escolha para o chamador)
MSalters
11
@MSalters com raiz, eu quis dizer a janela atual. Para obter todos os bloqueios que você precisa adquirir, você precisa subir a hierarquia, que exige o bloqueio de cada contêiner à medida que você encontra o pai e o desbloqueio (para garantir que você bloqueie apenas de cima para baixo) e depois espere que ele não tenha mudado. depois de obter a janela raiz e fazer o bloqueio de cima para baixo.
catraca aberração
@ratchetfreak: se a criança que você tenta bloquear é removida por outro segmento enquanto você o bloqueia, isso é um pouco infeliz, mas não é relevante para o bloqueio. Você simplesmente não pode operar em um objeto que outro thread acabou de remover. Mas por que esse outro thread está removendo objetos / janelas que seu thread ainda está usando? Isso não é bom em nenhum cenário, não apenas na interface do usuário.
MSalters 3/09/15
17

A threadedness (em um modelo de memória compartilhada) é uma propriedade que tende a desafiar os esforços de abstração. Um exemplo simples é a Setdo tipo: enquanto Contains(..)e Add(...)e Update(...)é uma API perfeitamente válida em um único cenário de rosca, o cenário multi-threaded precisa de um AddOrUpdate.

O mesmo se aplica à interface do usuário - se você deseja exibir uma lista de itens com uma contagem dos itens no topo da lista, é necessário atualizar os dois a cada alteração.

  1. O kit de ferramentas não pode resolver esse problema, pois o bloqueio não garante que a ordem das operações permaneça correta.
  2. A visualização pode resolver o problema, mas somente se você permitir que a regra de negócios de que o número no topo da lista tenha que corresponder ao número de itens da lista e atualize a lista somente através da visualização. Não é exatamente o que o MVC deveria ser.
  3. O apresentador pode resolvê-lo, mas precisa estar ciente de que a exibição tem necessidades especiais em relação à segmentação.
  4. A ligação de dados a um modelo com capacidade de multiencadeamento é outra opção. Mas isso complica o modelo com coisas que deveriam ser preocupações da interface do usuário.

Nada disso parece realmente tentador. Recomenda-se responsabilizar o apresentador pelo manuseio da segmentação, porque não é bom, mas porque funciona e as alternativas são piores.

Patrick
fonte
Talvez uma camada possa ser introduzida entre a View e o Presenter que também possa ser trocada.
precisa saber é o seguinte
2
O .NET precisa System.Windows.Threading.Dispatchermanipular o envio para os segmentos de interface do usuário WPF e WinForms. Esse tipo de camada entre o apresentador e a visualização é definitivamente útil. Mas ele apenas fornece independência ao kit de ferramentas, não enfraquece a independência.
Patrick Patrick
9

Há algum tempo, li um blog muito bom que discute esse problema (mencionado por Karl Bielefeldt), o que basicamente diz é que é muito perigoso tentar tornar o thread do kit de interface do usuário seguro, pois apresenta possíveis bloqueios e depende de como implementadas, condições de corrida no quadro.

Há também uma consideração de desempenho. Agora não muito, mas quando o Swing foi lançado, ele foi fortemente criticado por seu desempenho (ruim), mas isso não foi culpa do Swing, era falta de conhecimento das pessoas sobre como usá-lo.

O SWT aplica o conceito de segurança de encadeamento lançando exceções se você violá-lo, não é bonito, mas pelo menos você está ciente disso.

Se você observar o processo de pintura, por exemplo, a ordem na qual os elementos são pintados é muito importante. Você não deseja que a pintura de um componente tenha efeito colateral em qualquer outra parte da tela. Imagine se você pudesse atualizar a propriedade de texto de uma etiqueta, mas ela fosse pintada por dois segmentos diferentes, e você poderia acabar com uma saída corrompida. Portanto, toda a pintura é feita em um único encadeamento, normalmente com base na ordem de requisitos / solicitações (mas às vezes condensada para reduzir o número de ciclos físicos reais de pintura)

Você mencionou a mudança do Swing para o JavaFX, mas você teria esse problema com praticamente qualquer estrutura de interface do usuário (não apenas com clientes grossos, mas também com a Web). O Swing parece ser o que destaca o problema.

Você pode projetar uma camada intermediária (um controlador de um controlador?), Cuja tarefa é garantir que as chamadas para a interface do usuário sejam sincronizadas corretamente. É impossível saber exatamente como você pode projetar partes da API que não são da interface do usuário da perspectiva da API da interface do usuário e a maioria dos desenvolvedores reclamaria que qualquer proteção de thread implementada na API da interface do usuário era restritiva ou não atendia às suas necessidades. Melhor permitir que você decida como deseja resolver esse problema, com base em suas necessidades

Um dos maiores problemas que você precisa considerar é a capacidade de justificar uma determinada ordem de eventos, com base em informações conhecidas. Por exemplo, se o usuário redimensionar a janela, o modelo da Fila de Eventos garante que uma determinada ordem de eventos ocorrerá, isso pode parecer simples, mas se a fila permitir que eventos sejam acionados por outros encadeamentos, você não poderá mais garantir a ordem em em que os eventos podem ocorrer (uma condição de corrida) e, de repente, você precisa começar a se preocupar com diferentes estados e não fazer nada até que algo mais aconteça. Você começa a compartilhar bandeiras do estado e acaba com espaguete.

Ok, você pode resolver isso tendo algum tipo de fila que ordenou os eventos com base no horário em que foram emitidos, mas não é isso que já temos? Além disso, você ainda não pode garantir que o segmento B gere seus eventos DEPOIS do segmento A

A principal razão pela qual as pessoas ficam chateadas por ter que pensar em seu código é porque elas são feitas para pensar em seu código / design. "Por que não pode ser mais simples?" Não pode ser mais simples, porque não é um problema simples.

Lembro-me de quando o PS3 foi lançado e a Sony estava conversando com o processador Cell e sua capacidade de executar linhas separadas de lógica, decodificar áudio, vídeo, carregar e resolver dados do modelo. Um desenvolvedor de jogos perguntou: "Isso é incrível, mas como você sincroniza os fluxos?"

O problema de que o desenvolvedor estava falando era que, em algum momento, todos esses fluxos separados precisariam ser sincronizados em um único canal para saída. O pobre apresentador simplesmente deu de ombros, pois não era um problema que eles estavam familiarizados. Obviamente, eles tiveram soluções para resolver esse problema agora, mas ele tem graça na época.

Os computadores modernos estão recebendo muitas informações de vários lugares simultaneamente, todas essas informações precisam ser processadas e entregues ao usuário imediatamente, o que não interfere na apresentação de outras informações, por isso é um problema complexo, sem uma única solução simples.

Agora, tendo a capacidade de alternar estruturas, não é uma coisa fácil de projetar, mas, tome MVC por um momento, o MVC pode ser de várias camadas, ou seja, você pode ter um MVC que lida diretamente com o gerenciamento da estrutura da interface do usuário. poderia então envolver isso, novamente, em um MVC de camada superior que lida com interações com outras estruturas (potencialmente multiencadeadas), seria responsabilidade dessa camada determinar como a camada MVC inferior é notificada / atualizada.

Você usaria a codificação para fazer interface com padrões de design e padrões de fábrica ou construtor para construir essas diferentes camadas. Isso significa que as estruturas multithread são dissociadas da camada da interface do usuário através do uso de uma camada intermediária, como uma idéia.

MadProgrammer
fonte
2
Você não teria esse problema com a web. O JavaScript deliberadamente não tem suporte para threading - um tempo de execução JavaScript é efetivamente apenas uma grande fila de eventos. (Sim, eu sei que o JS, em termos estritos, tem WebWorkers - esses são tópicos nerfados e se comportam mais como atores em outros idiomas).
James_pic
11
@ James_pic O que você realmente tem é a parte do navegador que atua como o sincronizador da fila de eventos, que é basicamente o que estamos falando, o responsável pela chamada foi responsável por garantir que as atualizações ocorram com a fila de eventos dos kits de ferramentas
MadProgrammer
Sim, exatamente. A principal diferença, no contexto da Web, é que essa sincronização ocorre independentemente do código do chamador, porque o tempo de execução não fornece nenhum mecanismo que permita a execução do código fora da fila de eventos. Portanto, o chamador não precisa se responsabilizar por isso. Acredito que isso também é uma grande parte da motivação por trás do desenvolvimento do NodeJS - se o ambiente de tempo de execução é um loop de eventos, todo o código é reconhecido pelo loop de eventos por padrão.
James_pic
11
Dito isto, existem estruturas de interface do usuário do navegador que possuem seu próprio loop de eventos dentro de um loop de eventos (estou olhando para você, Angular). Como essas estruturas não são mais protegidas pelo tempo de execução, os chamadores também devem garantir que o código seja executado no loop de eventos, semelhante ao código multithread em outras estruturas.
James_pic
Haveria algum problema específico em ter controles "somente exibição" para fornecer segurança de thread automaticamente? Se alguém possui um controle de texto editável que é gravado por um thread e lido por outro, não há uma boa maneira de tornar a gravação visível para o thread sem sincronizar pelo menos uma dessas ações com o estado real da interface do usuário do controle, mas esses problemas importariam para os controles somente de exibição? Eu acho que eles poderiam facilmente fornecer qualquer bloqueio ou intertravamento necessário para as operações executadas neles, e permitir que o chamador ignore o tempo de quando ... #
23815