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ência | paralelismo | |
|---|---|---|
| o que é | estrutura: várias tarefas em andamento, intercaladas | execução: instruções no mesmo instante |
| exige vários núcleos | não | sim |
| resolve bem | espera por entrada e saída | cálculo intensivo |
| risco característico | ordem de execução e estado inconsistente entre retomadas | condição de corrida em memória compartilhada |
Assíncrono × paralelo × com bloqueio:
| Assíncrono | a chamada retorna antes de o trabalho terminar; o resultado chega depois |
| Com bloqueio | a thread para e espera o resultado |
| Paralelo | dois 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.
LidoPraticado