Como evitar condições de corrida assíncrona em JavaScript

Você pode ter uma corrida em um único segmento …

Fundo…

Eu comecei minha operadora de programação como um programador C #. Eu fazia parte de um projeto de back-end que tinha que executar bem sob carga de muitos pedidos, enquanto a maior parte do tratamento é IO em arquivos grandes, o que pode ser lento. Usamos tarefas para obter o desempenho necessário e tivemos que lidar com muitas lógicas assíncronas, bloqueios, gerenciamento de estados e assim por diante.

Depois de alguns anos, mudei para o desenvolvimento de aplicativos da web. Comecei com o TypeScript, uma vez que era uma mudança fácil do C #, mas tinha alguns projetos puramente JavaScript.

Lembro-me da minha primeira vez usando promessas e compreendendo que, embora o código seja assíncrono, você não precisa usar bloqueios para manipular o acesso multi-thread aos dados. Foi uma sensação incrível, eu não preciso bloquear coisas, para verificar e mudar para o thread da interface do usuário para alterar as coisas da interface do usuário, JavaScript é single threaded, foi incrível!

Mas por causa do meu passado, eu me senti enganado, deve haver algo que estou perdendo, não pode ser tão simples. Eu olhei como as coisas assíncronas funcionam no mundo do JavaScript e descobri as filas de mensagens. As filas de mensagens são usadas para comunicação entre trabalhadores em segundo plano e as mesmas filas são usadas para continuar o código assíncrono após a solicitação assíncrona. A idéia é que o JavaScript é singe threaded (a menos que usemos background workers), e quando alguma chamada assíncrona retorna, seu resultado é colocado em uma fila de mensagens, quando o thread principal terminar seu trabalho e livre (sem alternância de contexto, finalização completa) leva a próxima mensagem para lidar com isso.

Mais uma vez, me senti abençoado. Eu não preciso lidar com coisas assíncronas. Eu posso simplesmente escrever:

 buscar (url) 
.then (dados => data.json ())
. then (itens => updateState (itens));

Mas! (Há sempre um mas)

O problema…

Vamos pensar por um segundo, quando essa chamada de busca é invocada?

  • Toda vez que um usuário está clicando em um botão?
  • A cada X segundos?
  • A cada X pixels o mouse se move sobre um mapa?

Quanto tempo demora para a resposta retornar, no pior dos casos? 1,2,10 segundos? Talvez mais em rede muito lenta?

Vamos supor que temos um cenário como este:

  • Nós temos um mapa
  • Queremos mostrar algumas informações relacionadas à localização do usuário
  • Toda vez que um usuário está se movendo no mapa, buscamos os dados
  • Somos espertos e até debitamos as buscas por 0,5 segundos
  • Os dados são retornados após 5 segundos (estamos em rede lenta)

Agora vamos pensar no próximo caso:

  • O usuário abre o mapa
  • A primeira solicitação de busca é acionada
  • Após 2 segundos, o usuário se move no mapa novamente porque ele sentiu que encontrou algo interessante
  • Após outros 0,5 segundos, outra busca é disparada

Agora, se por algum motivo para a primeira busca levou 5 segundos para retornar e para a segunda busca levou 1 segundo para retornar (o sinal da rede ficou melhor ou o tamanho de busca / computação é muito menor ou não temos servidor ocupado para o segunda vez), o que acontecerá agora?

Bem, isso depende, uma coisa que sabemos com certeza, o updateState será chamado duas vezes, a primeira vez com a segunda resposta e a segunda com a primeira resposta.

Qual será o estado final da aplicação? Em um bom cenário, acumulamos as respostas e é realmente válido mostrar os dois resultados para o usuário. Em outro bom cenário, usamos algum cache e as duas respostas serão armazenadas em cache, e o mapa mostrará apenas os dados da área visualizada.

O pior caso (e provavelmente o real) é se, em cada chamada updateState, excluirmos o estado atual e definirmos o novo estado. Neste caso, o que acontecerá é um bug assíncrono estranho, nós olhamos para area2 mas vemos os itens de area1.

Meu ponto é que mesmo que você não precise gerenciar o código assíncrono usando bloqueios, você ainda precisa pensar sobre problemas assíncronos. Neste caso, uma condição de corrida. Quando vejo código JavaScript, ele geralmente não lida com isso.

Algumas soluções…

Então você me pergunta o que eu posso fazer?

  • Você pode salvar a última solicitação e cancelá-la na próxima. Para o momebt de escrever este artigo buscar não tem uma API de cancelamento ( https://github.com/whatwg/fetch/issues/27 ). Mas por causa do argumento aqui está um código com setTimeout:
 if (this.lastRequest) { 
clearTimeout (this.lastRequest);
}
 this.lastRequest = setTimeout (() => { 
updateState ([1,2,3]);
this.lastRequest = nulo;
}, 5000);

  • Você pode criar um objeto de sessão a cada nova solicitação e salvá-lo e, em resposta, verificar se o salvo ainda é o mesmo que você possui:
 const currentSession = {}; 
this.lastSession = currentSession;
 buscar (url) 
.then (dados => data.json ())
. then (items => {
if (this.lastSession! == currentSession) {
Retorna;
}
updateState (itens);
}). catch (erro => {
if (this.lastSession! == currentSession) {
Retorna;
}
setError (erro);
});

Aqui o objeto currentSession de cada pedido é salvo no fechamento do pedido ea lastSession é salvo sobre este assunto. Não se esqueça do tratamento de erros , você não quer mostrar um erro quando não há nenhum.

Embora ambos os métodos cuidem da condição de corrida, no primeiro método (cancelamento) ainda há um caso em que haverá duas respostas retornadas:

  • O primeiro pedido está a caminho
  • A segunda solicitação está no processo, a instrução if é avaliada
  • Agora a primeira resposta retornada e é colocada na fila de mensagens
  • O segundo pedido é demitido

Agora, a primeira e a segunda solicitações serão tratadas na mesma ordem em que foram disparadas. Isso ainda pode causar erros, por exemplo, se no método updateState você definir o indicador isLoading como false, o usuário receberá os itens da primeira solicitação e, após algum tempo, sem aviso, os outros itens aparecerão.

É por isso que geralmente é melhor combinar os dois.

  • Cancelar a solicitação anterior desnecessária: liberará o servidor de manipular a solicitação desnecessária
  • Em um caso em que apenas a última solicitação é necessária, salve um objeto de sessão e manipule apenas a solicitação com a sessão correta.

Resumo…

Este artigo não é sobre dar-lhe a melhor solução para a condição de corrida assíncrona, não é mesmo sobre a condição de corrida, escrevo este artigo para torná-lo ciente de problemas assíncronos que podem ocorrer mesmo pensamento JavaScript é único threaded, coisas que você precisa pense quando estiver trabalhando com coisas assíncronas.

Não há sempre um problema, às vezes o problema existe, mas é reproduzido em condições tão raras que você não se importa ou a correção custa muito e não dá quase nada, mas você precisa estar ciente dos problemas que você tem em ordem. para tomar a decisão certa.

Espero ter ajudado pelo menos alguém … =]