Por que a responsabilidade do responsável pela chamada é garantir a segurança do thread na programação da GUI?

37

Eu tenho visto, em muitos lugares, que é a sabedoria canônica 1 que é de responsabilidade do responsável pela chamada 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 Event Dispatch Thread ).

Por que isso é assim? O Thread de Envio de Eventos é uma preocupação da view no MVC / MVP / MVVM; para manipulá-lo em qualquer lugar, mas a visualização cria um acoplamento strong entre a implementação da exibição e o modelo de segmentação da implementação dessa exibição.

Especificamente, digamos que eu tenha um aplicativo arquitetado pelo MVC que usa o Swing. Se o responsável pela chamada atualizar os componentes no Event Dispatch Thread, se eu tentar trocar minha implementação do Swing View por uma implementação do JavaFX, devo alterar todo o código do Presenter / Controller para usar o encadeamento do Aplicativo JavaFX .

Então, suponho que tenho duas perguntas:

  1. Por que a responsabilidade do responsável pela chamada é garantir a segurança do thread do componente de interface do usuário? Onde está a falha no meu raciocínio acima?
  2. Como posso projetar meu aplicativo para ter um baixo acoplamento dessas questões de segurança de thread, mas ainda assim ser apropriadamente thread-safe?

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

Quem chama é 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 sendo 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 pontuação mais alta em Swing no Stack Overflow. Ele diz isso em todo o lugar e eu também vi a responsabilidade do chamador em outros lugares também.

    
por durron597 02.09.2015 / 21:00
fonte

4 respostas

22

No final de seu ensaio de sonhos com falha , Graham Hamilton (a arquiteto Java) menciona se os desenvolvedores "devem preservar a equivalência com um modelo de fila de eventos, eles precisarão seguir vários regras não óbvias, "e ter um modelo de fila de eventos visível e explícito" parece ajudar as pessoas a seguir o modelo de forma mais confiável e, assim, construir programas GUI que funcionem de forma 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 formas não óbvias que são 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 requer vários bloqueios, será muito difícil raciocinar sobre como a camada multithread e a camada de fila de eventos interagem.

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

    
por 03.09.2015 / 00:03
fonte
25

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

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

Criar uma estratégia de bloqueio que não bloqueie a janela raiz (causará muita contenção) é difícil . Bloquear de baixo para cima enquanto outra thread está bloqueando de cima para baixo é uma ótima maneira de alcançar um impasse instantâneo.

E verificar se o thread atual é o thread de gui cada vez é caro e pode causar confusão sobre o que realmente está acontecendo com o gui, especialmente quando você está fazendo uma sequência de gravação de read read. Isso precisa ter um bloqueio nos dados para evitar as corridas.

    
por 02.09.2015 / 21:23
fonte
17

Threadedness (em um modelo de memória compartilhada) é uma propriedade que tende a desafiar os esforços de abstração. Um exemplo simples é um Set -type: enquanto Contains(..) e Add(...) e Update(...) é uma API perfeitamente válida em um único cenário encadeado, o cenário de vários segmentos precisa de 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, você precisa atualizar ambos em 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 exibição pode resolver o problema, mas somente se você permitir que a regra de negócios mostre que o número no topo da lista deve corresponder ao número de itens na lista e atualizar apenas a lista por meio da exibição. Não é exatamente o que o MVC deveria ser.
  3. O apresentador pode resolvê-lo, mas precisa saber que a exibição tem necessidades especiais em relação ao encadeamento.
  4. A vinculação de dados a um modelo com capacidade para multi-threading é outra opção. Mas isso complica o modelo com coisas que deveriam ser preocupações com a interface do usuário.

Nada disso parece realmente tentador. Tornar o apresentador responsável pelo manuseio de threads não é recomendado porque é bom, mas porque funciona e as alternativas são piores.

    
por 02.09.2015 / 21:42
fonte
9

Eu li um blog muito bom há algum tempo atrás que discute esta questão (mencionado por Karl Bielefeldt), o que basicamente diz é que é muito perigoso tentar fazer o thread do kit de UI seguro, pois ele introduz possíveis deadlocks e dependendo como ela é implementada, condições de corrida no framework.

Há também uma consideração de desempenho. Não muito agora, mas quando Swing foi lançado pela primeira vez, foi strongmente criticado por sua performance (foi ruim), mas isso não foi culpa de Swing, foi a falta de conhecimento das pessoas sobre como usá-lo.

O SWT impõe o conceito de segurança de thread lançando exceções se você violar, não é bonito, mas pelo menos você está ciente disso.

Se você observar o processo de pintura, por exemplo, a ordem em que os elementos são pintados é muito importante. Você não quer que a pintura de um componente tenha um efeito colateral em qualquer outra parte da tela. Imagine se você pudesse atualizar a propriedade de texto de um rótulo, mas ele fosse pintado por dois segmentos diferentes, você poderia acabar com uma saída corrompida. Assim, toda a pintura é feita dentro de um único encadeamento, normalmente baseado na ordem de requisitos / solicitações (mas às vezes condensado para reduzir o número de ciclos reais de pintura física)

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 clientes thick, mas também web), o Swing parece ser o único que destaca o problema.

Você poderia projetar uma camada intermediária (um controlador de um controlador?) cujo trabalho é garantir que as chamadas para a interface do usuário sejam sincronizadas corretamente. É impossível saber exatamente como você pode projetar suas partes não-UI de sua API a partir 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 quer 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 entradas conhecidas. Por exemplo, se o usuário redimensiona a janela, o modelo Event Queue garante que uma determinada ordem de eventos ocorrerá, isso pode parecer simples, mas se a fila permitia que os eventos fossem acionados por outros threads, você não pode mais garantir a ordem em que os eventos podem ocorrer (uma condição de corrida) e repentinamente você tem que começar a se preocupar com estados diferentes e não fazer nada até que algo mais aconteça e você comece a ter que compartilhar bandeiras do estado e acabe com spaghetti.

Ok, você pode resolver isso com algum tipo de fila que ordenou os eventos com base na hora em que foram emitidos, mas não é isso que já temos? Além disso, você ainda não poderia garantir que o thread B iria gerar seus eventos AFTER thread A

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

Eu me lembro quando o PS3 foi lançado e a Sony estava falando sobre o processador Cell e sua capacidade de executar linhas separadas de lógica, decodificar áudio, vídeo, carregar e resolver dados de modelo. Um desenvolvedor de jogos perguntou: "Tudo isso é incrível, mas como você sincroniza os streams?"

O problema sobre o qual o desenvolvedor estava falando era que, em algum momento, todos esses fluxos separados precisariam ser sincronizados em um único canal para a 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 é engraçado na época.

Os computadores modernos estão recebendo muitas informações de diversos lugares simultaneamente, toda essa entrada precisa ser processada e entregue ao usuário, o que não interfere na apresentação de outras informações, então é um problema complexo. , sem uma única solução simples.

Agora, tendo a capacidade de alternar estruturas, não é fácil projetar, mas, por exemplo, MVC, o MVC pode ser multicamada, ou seja, você pode ter um MVC que lide diretamente com o gerenciamento da interface do usuário framework, você poderia então envolver isso, novamente, em uma camada superior MVC que lida com interações com outras estruturas (potencialmente multi thread), seria de responsabilidade desta camada determinar como a camada mais baixa do MVC é notificada / atualizada.

Você usaria a codificação para fazer interface dos padrões de design e dos padrões de fábrica ou de construtor para construir essas diferentes camadas. Isso significa que as estruturas multithread tornam-se desacopladas da camada da interface do usuário por meio do uso de uma camada intermediária, como uma ideia.

    
por 03.09.2015 / 02:34
fonte