← tópicos

Específicos · Engenharia de Software

Programação assíncrona: concorrência × paralelismo, event loop, promises, GIL

Concorrência é lidar com várias tarefas em andamento; paralelismo é executá-las no mesmo instante — e o Node.js é o caso-limite: uma única thread de JavaScript, altamente concorrente porque nunca…

Altíssima4 itens no tópico

A ideia que organiza o assunto

Duas palavras que o uso corrente trata como sinônimas e que a prova separa. Concorrência é uma propriedade da estrutura do programa: várias tarefas estão em andamento ao mesmo tempo, intercaladas, mesmo que apenas uma execute a cada instante. Paralelismo é uma propriedade da execução: duas instruções rodam no mesmo instante físico, e isso exige mais de uma unidade de processamento. Uma única thread pode ser altamente concorrente e não ter paralelismo nenhum; um programa pode ser paralelo sem ter concorrência interessante, como ao dividir uma multiplicação de matrizes entre núcleos.

Daí a pergunta que resolve quase todo item: o que está limitando o programa? Se ele passa o tempo esperando — resposta de rede, leitura de disco, consulta ao banco —, o gargalo não é processador, e a solução é não esperar: registrar o que fazer quando a resposta chegar e seguir para a próxima tarefa. Isso é programação assíncrona, e resolve com uma thread o que exigiria centenas de threads bloqueadas. Se ele passa o tempo calculando, nenhuma assincronia ajuda: só dividir o cálculo entre núcleos, com threads ou processos, encurta o tempo — e aí aparecem os problemas de memória compartilhada, condição de corrida e exclusão mútua.

O Node.js é o exemplo em que a banca encena essa distinção. O JavaScript da aplicação roda em uma única thread, sobre um laço de eventos: cada operação de entrada e saída é despachada sem bloquear e devolve o controle imediatamente; quando o resultado fica pronto, o retorno é enfileirado e executado. Um único fluxo de execução atende milhares de conexões simultâneas porque nenhuma delas o faz parar.

Por que se usa (e o que custa)

O modelo assíncrono compra escala em entrada e saída com pouquíssima memória: não há pilha nem troca de contexto por conexão, apenas uma estrutura de continuação por operação pendente. Compra também ausência de disputa: com uma thread só, não há condição de corrida sobre a memória da aplicação, nem necessidade de travas.

Paga em três frentes. Qualquer trabalho pesado de processador bloqueia todo o resto, porque não existe outra thread para atender enquanto isso — cálculo demorado num servidor de laço de eventos trava todas as conexões e é o defeito clássico do modelo. O controle de fluxo fica mais difícil de ler: retorno encadeado dentro de retorno, e é para isso que existem as promessas e a sintaxe de espera. E o erro deixa de subir naturalmente: numa promessa, a falha não vira exceção do fluxo principal; ela rejeita a promessa, e quem não tratar a rejeição perde o erro silenciosamente.

O modelo com threads faz a troca inversa: aproveita vários núcleos e tolera bloqueio, mas paga em memória por thread, troca de contexto e, sobretudo, em sincronização — dado compartilhado e mutável exige exclusão mútua, e errar a trava produz condição de corrida ou impasse.

Como funciona

Laço de eventos e não bloqueio. A thread de JavaScript executa o código até o fim do trecho corrente; operações de entrada e saída são delegadas ao sistema e a uma reserva interna de threads do ambiente de execução, não ao programa. Quando terminam, seus retornos entram numa fila e são processados pelo laço de eventos. Nada disso torna a aplicação paralela: continua havendo um fluxo de execução de JavaScript por vez, e é exatamente por isso que o código do usuário não precisa de travas.

Promessas. Uma promessa representa um valor que ainda não existe e tem três estados: pendente, resolvida e rejeitada. Quem a cria decide o desfecho chamando o resolvedor ou o rejeitador; quem a consome registra o tratamento com then para o caminho de sucesso e catch para o de falha. O par async e await é açúcar sobre isso: a função marcada como assíncrona devolve uma promessa, e a espera suspende aquela função sem bloquear a thread, que continua atendendo outros eventos. A rejeição não interrompe o programa — ela percorre a cadeia até o primeiro tratador; sem tratador, fica sem tratamento.

Suporte assíncrono nas ferramentas de teste. Os arcabouços de teste precisam esperar preparação assíncrona antes de rodar os casos. No Jasmine, os ganchos de preparação e de limpeza — entre eles o que roda uma vez antes de todos os testes — aceitam função que devolve promessa ou que use async e await, e o arcabouço só prossegue quando a promessa se resolve. Sem isso, o teste começaria antes de a preparação terminar.

Memória compartilhada, quando há paralelismo de verdade. Com mais de uma thread sobre o mesmo dado, duas necessidades aparecem e não são a mesma coisa: compartilhar a posse do dado entre as threads e garantir que apenas uma o altere por vez. Em Rust essa separação é explícita: a contagem atômica de referências resolve a posse compartilhada — vários donos, liberação quando o último sai — e dá acesso somente de leitura; a exclusão mútua é outro tipo, que envolve o dado e obriga a adquirir a trava para alterá-lo. Por isso um contador compartilhado e mutável exige os dois envolvendo um ao outro: a contagem de referências por fora, a trava por dentro.

O que decide os itens

concorrênciaparalelismo
o que éestrutura: várias tarefas em andamento, intercaladasexecução: instruções no mesmo instante
exige vários núcleosnãosim
resolve bemespera por entrada e saídacálculo intensivo
risco característicoordem de execução e estado inconsistente entre retomadascondição de corrida em memória compartilhada

Assíncrono × paralelo × com bloqueio:

Assíncronoa chamada retorna antes de o trabalho terminar; o resultado chega depois
Com bloqueioa thread para e espera o resultado
Paralelodois trabalhos progridem fisicamente ao mesmo tempo

Fronteiras que a banca repete: Node.js executa o JavaScript da aplicação em uma única thread, e é assíncrono e não bloqueante por isso, não apesar disso; assincronia não é paralelismo e não acelera cálculo; promessa rejeitada vai para o catch, e promessa resolvida para o then; comparação estrita em JavaScript não converte tipo — número comparado a texto nunca é igual; e contagem de referências compartilhada não é exclusão mútua.

Como a CEBRASPE derruba você aqui

A medição está fechada, e são quatro itens. Os quatro itens do tópico estão explicados: dois Certos e dois Errados. Dois itens errados não desenham frequência nenhuma, e os dois nem sequer se parecem: um pede o resultado de um código com promessa, o outro funde duas garantias de concorrência numa construção só de Rust. O que segue descreve o que cada um faz, item a item; o que é apenas conhecido do assunto vem rotulado como não medido.

Pedir o resultado de um trecho de código e contar com a desatenção. É o que faz um dos dois itens errados. O enunciado exibe uma promessa com rejeição e resolução e afirma qual mensagem será impressa — “4 não é primo”. O que decide o item é um detalhe pequeno do código, e não a teoria: a comparação number % i === "0" é estrita entre número e texto, nunca é verdadeira, a rejeição do laço nunca ocorre e o catch não é acionado. Defesa: execute o código mentalmente linha a linha e, principalmente, decida qual dos dois caminhos a promessa toma; a mensagem anunciada é a do caminho que não é percorrido. Um único item não faz formato dominante, mas o desenho é o que a banca repete em toda a família de itens com código.

Somar propriedades de tipos vizinhos numa frase só. No item de Rust, a contagem atômica de referências aparece descrita como se também fizesse exclusão mútua e garantisse acesso exclusivo. Cada metade nomeia um mecanismo real, e a frase os funde. Sempre que um item atribuir duas garantias diferentes a uma única construção, separe-as e pergunte qual tipo, qual função ou qual biblioteca entrega cada uma.

Explorar a confusão entre uma thread e pouco desempenho. Os dois itens Certos do tópico são descrições sóbrias que parecem ter algo de errado e não têm: o gancho beforeAll do Jasmine aceitando promessa e função assíncrona, e o Node.js descrito como de thread única e, ao mesmo tempo, não bloqueante. O segundo parece contraditório e é verdadeiro: a concorrência vem de nunca bloquear, não de multiplicar threads. O erro aparece na formulação inversa, quando se atribui ao modelo assíncrono um ganho em processamento paralelo que ele não tem.

Erros clássicos

Achar que assíncrono significa paralelo. Assíncrono é não esperar; paralelo é executar ao mesmo tempo. O código assíncrono de thread única não usa dois núcleos e não torna um cálculo mais rápido.

Achar que o Node.js é lento por ter uma thread. Ele é rápido em cargas de entrada e saída justamente por isso: sem bloqueio e sem troca de contexto por conexão. O que o derruba é trabalho pesado de processador na thread do laço de eventos.

Esquecer que a comparação estrita não converte tipos. Em JavaScript, um número comparado com igualdade estrita a um texto é sempre diferente, ainda que os caracteres pareçam iguais. Em item de código, é o tipo de detalhe que inverte o desfecho inteiro.

Deixar a promessa rejeitada sem tratamento. Rejeição não levanta exceção no fluxo que criou a promessa: ela percorre a cadeia até um catch. Sem tratador, o erro não aparece onde se espera.

Confundir posse compartilhada com exclusão mútua. Compartilhar a referência permite que várias threads enxerguem o dado; alterar com segurança exige trava. Um contador compartilhado e mutável precisa das duas coisas, uma dentro da outra.

Praticar4 itens