Algumas informações sobre quem está a escrever isto e porquê. Somos a Geonode e vendemos proxies, o que nos coloca constantemente em contacto com este tema — o web scraping é a carga de trabalho por excelência limitada pela E/S, e «o meu scraper é lento» é uma das queixas mais comuns que as pessoas nos apresentam. Por isso, eis a versão honesta antes da teoria: se o seu scraper é lento porque recupera uma página de cada vez, comprar proxies não o vai acelerar. A concorrência é uma propriedade do seu código. Cem endpoints de proxy e um loop sequencial resultam num scraper sequencial com cem endpoints inativos. Resolva primeiro a questão da concorrência. Os proxies resolvem um problema diferente — aquele com que se depara depois de a concorrência funcionar, quando o alvo começa a limitar a taxa do end-point que, de repente, está a fazer cinquenta pedidos por segundo. Ambos os problemas são reais. Não são o mesmo problema e não têm a mesma solução.
Dito isto, a distinção.
A distinção numa frase
A formulação de Rob Pike, retirada da sua palestra sobre o tema, continua a ser a mais clara, e o blog do Go afirma-o diretamente:
Concorrência é «a composição de processos que se executam de forma independente».
Paralelismo é «a execução simultânea de cálculos (possivelmente relacionados)».
Leia estas definições duas vezes, porque a diferença reside no ponto em que se coloca a ênfase. A concorrência diz respeito à estrutura — à forma como se decompõe um problema em partes que podem avançar de forma independente. O paralelismo diz respeito à execução — quantas dessas partes são executadas fisicamente no mesmo instante.
A consequência que as pessoas ignoram: a concorrência é algo que se escreve, o paralelismo é algo que a máquina faz. Pode-se escrever um programa concorrente e executá-lo num único núcleo, onde nada é simultâneo, e ele continuará a ser concorrente. Descreveu tarefas independentes; o ambiente de execução intercala-as. E um programa concorrente executado em oito núcleos pode tornar-se paralelo, se o ambiente de execução e a carga de trabalho o permitirem.
Essa assimetria é a razão pela qual as duas palavras não são intercambiáveis. A concorrência permite o paralelismo sem o garantir. O paralelismo sem uma estrutura concorrente não é de todo possível.
Por que é que a confusão persiste
Três razões, e identificá-las ajuda.
O comportamento observável é frequentemente idêntico. Um programa concorrente num núcleo e um programa paralelo em quatro parecem ambos «várias coisas a acontecerem». Visto de fora, não é possível distinguir qual dos dois se tem. Só se descobre quando se adicionam núcleos e nada melhora.
O vocabulário de cada linguagem é inconsistente. O módulo threading do Python proporciona concorrência, mas, historicamente, não proporciona paralelismo. O multiprocessing do Python proporciona ambos. O async / await do JavaScript proporciona concorrência, mas nunca paralelismo para o seu próprio código. As goroutines do Go proporcionam concorrência e paralelismo até ao nível de GOMAXPROCS. As mesmas palavras na documentação têm significados diferentes nos diferentes ecossistemas.
Na maioria das vezes, não precisa de se preocupar com isso, até que, de repente, tenha de o fazer. Para uma carga de trabalho limitada pela E/S, a distinção é quase académica — a concorrência, por si só, já lhe dá toda a vantagem. Para uma carga de trabalho limitada pela CPU, é uma questão totalmente diferente, porque a concorrência, por si só, não traz qualquer benefício. O problema é que as pessoas aprendem o padrão que funcionou no seu problema limitado pela E/S e aplicam-no a um problema limitado pela CPU.
Como cada linguagem o faz na prática
| Tempo de execução | Mecanismo de concorrência | Paralelismo real para o seu código | Onde falha |
|---|
| Python (compilação padrão) | threading, asyncio | Apenas através de multiprocessing | O GIL serializa a execução do bytecode |
| Python (compilação com threads livres) | threading, asyncio | Sim, as threads executam-se em paralelo | Sobrecarga de thread única, maturidade do ecossistema |
| Go | goroutines + canais | Sim, até GOMAXPROCS | O estado partilhado continua a necessitar de sincronização |
| Node.js | ciclo de eventos, async/await | Apenas através de worker_threads ou processos filhos | Uma chamada de retorno limitada pela CPU bloqueia tudo |
| Java / C# | threads, conjuntos de threads | Sim | Complexidade do estado partilhado mutável |
| Rust | async + threads | Sim | O compilador obriga-o a estar correto desde o início |
O Node.js é a ilustração mais clara de concorrência sem paralelismo. A documentação oficial explica-o com precisão: o ciclo de eventos «permite que o Node.js execute operações de E/S não bloqueantes — apesar de, por predefinição, ser utilizada uma única thread de JavaScript —, transferindo as operações para o kernel do sistema sempre que possível». O kernel é multithread; o seu JavaScript não é. O ciclo passa por seis fases — temporizadores, callbacks pendentes, inativo/preparação, sondagem, verificação, encerramento de callbacks — e, quando uma operação termina, «o kernel informa o Node.js para que o callback apropriado possa ser adicionado à fila de sondagem para, eventualmente, ser executado».
A consequência prática decorre diretamente daí. Dez mil pedidos HTTP simultâneos no Node são triviais, porque a espera ocorre no kernel. Uma função que consome muitos recursos da CPU e que demora dois segundos a executar-se congela todo o processo, porque existe apenas uma thread para a executar e nada pode preemptá-la.
O Go adota a abordagem oposta: as goroutines são suficientemente baratas para serem criadas aos milhares, e o agendador distribui-as pelas threads do SO até umGOMAXPROCSo, cujo valor por predefinição é o número de núcleos disponíveis. Assim, o Go proporciona-lhe concorrência e paralelismo a partir da mesma construção. É por isso que o Pike fez a palestra — a distinção é mais importante numa linguagem em que se obtêm ambas e, por isso, é possível confundi-las.
A questão decisiva: o seu trabalho está limitado pela E/S ou pela CPU?
Tudo o que foi referido acima resume-se a uma questão sobre a sua carga de trabalho, e vale a pena medir em vez de partir de suposições.
Limitado pela E/S significa que o seu programa passa a maior parte do tempo à espera: de uma resposta de rede, de uma leitura do disco, de uma consulta à base de dados. Durante a espera, a CPU fica ociosa. A concorrência é a resposta correta e suficiente, porque permite iniciar a próxima espera enquanto a atual ainda está pendente. O paralelismo não acrescenta essencialmente nada — oito núcleos à espera da rede não são mais rápidos do que um núcleo à espera da rede.
Limitado pela CPU significa que o seu programa passa a maior parte do tempo a processar: a analisar, a comprimir, a aplicar funções hash, a transformar. A CPU está saturada. A concorrência, por si só, não altera nada — intercalar dois cálculos num único núcleo demora o mesmo tempo total que executá-los em sequência, mais a sobrecarga da comutação. Apenas o paralelismo ajuda, e apenas até ao número de núcleos físicos.
Para descobrir qual é o seu caso, meça em vez de raciocinar. No Linux, o comando «time» dá-lhe a resposta imediatamente: compare o tempo real decorrido com o tempo de CPU do utilizador mais o do sistema. Se o tempo real for muito superior ao tempo de CPU, está a esperar — limitado pela E/S. Se forem próximos, está a processar — limitado pela CPU.
A extração de dados da Web é um exemplo útil porque envolve ambos, em sequência. A recuperação de páginas é fortemente limitada pela E/S; a análise do HTML a seguir é limitada pela CPU. A arquitetura correta utiliza concorrência para a recuperação e paralelismo para a análise, sendo que o erro comum é aplicar uma única estratégia a ambas as fases. Um scraper com 200 recuperações simultâneas a alimentar um analisador de thread único não é um scraper rápido — é um recuperador rápido com uma fila a acumular-se atrás dele.
O GIL do Python e o que a implementação de threads livres alterou
O Python merece uma secção própria, pois a sua situação mudou significativamente e grande parte do que irá ler sobre o assunto está agora desatualizado.
Historicamente: o Global Interpreter Lock (GIL) do CPython permitia que apenas um thread executasse o bytecode do Python de cada vez. Os threads proporcionavam, portanto, concorrência, mas não paralelismo. O GIL é liberado durante as operações de E/S, pelo que a E/S com threads funcionava bem; o uso de threads em tarefas dependentes da CPU não funcionava, e a função multiprocessing era a solução alternativa.
O que mudou: a partir da versão 3.13, o CPython inclui uma compilação opcional com o GIL desativado. A documentação sobre threads livres descreve-o de forma clara — «A execução com threads livres permite a utilização total da potência de processamento disponível, executando threads em paralelo nos núcleos de CPU disponíveis.»
A PEP 779, aceite pelo Conselho Diretivo a 16 de junho de 2025 com o estatuto «Final», definiu os critérios para passar a execução com threads livres do estado «experimental» para «oficialmente suportado», tendo como meta o Python 3.14 para essa fase.
Quatro pontos práticos a ter em conta antes de a utilizar:
Não é a compilação predefinida. Tem de a obter ou compilar deliberadamente — a partir do código-fonte, o que significa a opção de configuração «--disable-gil». Verifique o que está a executar com «python -VV», que mostra «free-threading build», ou «sys._is_gil_enabled()», que devolve «False» quando o GIL está desativado.
O código de thread único fica mais lento. A documentação indica que, no conjunto de testes pyperformance, «a sobrecarga média varia entre cerca de 1% no macOS aarch64 e 8% em sistemas Linux x86-64». A PEP 779 regista que o Conselho Diretivo espera que o Python com threads livres «seja cerca de 10 a 15% mais lento», com 15% como meta rígida para a fase II, e aceita um aumento de 20% na média geométrica do uso de memória como «o custo de se ter threads livres eficientes e seguras». Se o seu programa for de thread único, esta compilação representa uma regressão direta.
Pode reativar o GIL em tempo de execução. As compilações com threads livres suportam a execução com o GIL ativado através da variável de ambiente PYTHON_GIL ou da opção -X gil — útil quando uma dependência apresenta comportamento indesejado.
As suas dependências são a restrição. As extensões C têm de ser compiladas de forma a declarar suporte para multithreading. O ecossistema evoluiu substancialmente, mas «funciona na minha máquina com Python puro» não é o mesmo que «a minha pilha científica funciona».
No caso de operações limitadas pela E/S, que predominam no scraping e no trabalho com APIs, nada disto altera a sua decisão: asyncio ou um conjunto de threads na compilação padrão já lhe oferece tudo o que a concorrência pode proporcionar. O suporte a threads livres é importante quando a fase de análise, e não a de obtenção de dados, é o seu gargalo.
Um exemplo prático: recuperar 10 000 URLs
Os números concretos tornam a diferença evidente. Suponhamos que cada pedido demore 200 ms e que a análise de cada resposta consuma 50 ms de CPU, numa máquina de quatro núcleos.
Sequencial. 10 000 × 250 ms = 2 500 segundos, cerca de 42 minutos. A CPU fica inativa durante 80% desse tempo.
Recuperação simultânea, análise sequencial. Com 100 pedidos simultâneos, a recuperação reduz-se para cerca de 20 segundos de tempo real. A análise permanece inalterada: 10 000 × 50 ms = 500 segundos. Total de cerca de 520 segundos, aproximadamente 9 minutos. Uma melhoria de 4,8× — e repare onde foi parar o tempo. A recuperação representava 80% do tempo de execução original e agora representa 4% do novo. A análise, que não alterou, representa agora 96% do tempo total.
Recuperação simultânea, análise paralela em quatro núcleos. A análise desce para cerca de 125 segundos. Total de cerca de 145 segundos, aproximadamente 2,5 minutos. Uma melhoria de 17 vezes em relação à execução sequencial.
Há três lições a retirar destes números.
Primeiro, o maior ganho advém da correção da E/S com concorrência, e é praticamente gratuito — sem núcleos adicionais, sem problemas de estado partilhado, apenas um ciclo diferente.
Segundo, assim que se corrige o gargalo dominante, o seguinte passa imediatamente a dominar. Esta é a Lei de Amdahl na sua forma mais prática: otimizar uma etapa que representa 20% do tempo de execução não pode tornar o código mais de 25% mais rápido, por mais completamente que a elimine. Meça sempre antes de otimizar e volte a medir depois, porque a resposta muda.
Em terceiro lugar — e é aqui que o nosso interesse comercial se torna relevante, por isso pondere-o em conformidade — no momento em que passa de uma solicitação de cada vez para cem, torna-se visível. Um único endereço a fazer 500 solicitações por segundo a um anfitrião será limitado na taxa e, em seguida, bloqueado. Isso não é um problema de simultaneidade e nenhuma quantidade de «asyncio» o resolve; é um problema de distribuição, e é para isso que servem os proxies. O nosso tráfego residencial começa nos 0,79 $/GB e o do centro de dados nos 0,14 $/GB, conforme verificado na nossa página de preços em setembro de 2026. Mas repare na ordem: primeiro a simultaneidade, depois os proxies quando a simultaneidade cria um problema que não consegue resolver. Fazer o contrário significa pagar por largura de banda que não tem como utilizar.
Quando a concorrência deixa de ser vantajosa
Aumentar a concorrência traz retornos decrescentes e, posteriormente, negativos, e o ponto de viragem chega mais cedo do que a maioria das pessoas espera.
Limites de ligação. Os sistemas operativos limitam o número de descritores de ficheiros abertos. Os servidores limitam o número de ligações simultâneas por cliente. Dez mil pedidos simultâneos provenientes de uma única máquina atingirão um destes limites muito antes de atingirem o limite da CPU, e o modo de falha é normalmente um erro confuso, em vez de um erro claro.
Memória. Cada pedido em curso mantém buffers, cabeçalhos analisados e dados de resposta pendentes. Dez mil pedidos simultâneos, cada um com 100 KB, representam um gigabyte de memória que não faz nada além de esperar.
Sobrecarga da alternância de contexto. As threads do sistema operativo não são gratuitas — cada uma acarreta uma pilha e um custo de agendamento. É exatamente por isso que as goroutines e as coroutines existem: são suficientemente económicas para que milhares sejam razoáveis, ao passo que milhares de threads do sistema operativo não o são.
A tolerância do destino. A outra extremidade da ligação tem as suas preferências. Para além de uma determinada taxa, a concorrência adicional produz erros 429 e 503 em vez de dados, e a sua taxa de transferência efetiva diminui à medida que se adiciona mais. Este é o limite máximo mais comum no mundo real e o menos frequentemente medido, porque as solicitações ainda «funcionam» — apenas devolvem erros que um ciclo de repetição repete diligentemente.
A abordagem prática não é nada glamorosa: comece com um limite de concorrência modesto, meça as «solicitações concluídas por segundo» em vez das «solicitações tentadas» e aumente até que a taxa de transferência deixe de melhorar. Ela estabilizará e, em seguida, diminuirá. O ponto ótimo situa-se nesse patamar e é, normalmente, um número muito menor do que a intuição sugere — muitas vezes, dezenas em vez de centenas.
Quando não é necessário nenhuma das duas coisas
Vale a pena referir isto, porque «torná-lo simultâneo» tornou-se um reflexo.
Quando o trabalho é realmente pequeno. Cem pedidos que demoram 200 ms cada um equivalem a 20 segundos em execução sequencial. Se for executado todas as noites numa tarefa cron, 20 segundos é suficiente e o código concorrente é mais difícil de depurar quando falha às 3 da manhã.
Quando a ordem faz parte dos requisitos. Alguns pipelines têm de processar itens numa ordem rigorosa, ou cada etapa depende do resultado anterior. A concorrência, neste caso, não é apenas inútil; é uma fonte de erros que só surgem sob carga.
Quando o estrangulamento está num local completamente diferente. Se as gravações na sua base de dados forem a restrição, 200 leitores simultâneos apenas criam uma fila mais longa à frente do mesmo bloqueio. Resolva o verdadeiro gargalo. A concorrência a montante de um recurso serial transforma um programa lento num programa lento com um problema de memória.
Quando o estado partilhado é complicado. O código simultâneo que interfere no estado partilhado mutável necessita de sincronização, e se isso for mal feito, produz-se a pior classe de erros — intermitentes, dependentes da carga e irreproduzíveis na sua máquina. Se o aumento de velocidade for de 2× e o estado for complexo, o código sequencial, que permite um raciocínio lógico, é frequentemente a melhor decisão de engenharia.
E a versão relevante para nós: se estiver a extrair algumas centenas de páginas por dia de um site que não se importa com isso, não precisa nem de concorrência nem de proxies. Uma chamada requests num ciclo com um atraso educado é a resposta certa, e preferimos dizer-lhe isso do que vender-lhe um plano de que não precisa.
Perguntas frequentes
Qual é a diferença mais simples entre concorrência e paralelismo?
A concorrência consiste em lidar com várias coisas ao mesmo tempo — uma propriedade estrutural da forma como se escreve o programa. O paralelismo consiste em fazer várias coisas ao mesmo tempo — uma propriedade física da forma como o programa é executado. A concorrência torna o paralelismo possível; não é ela que o faz acontecer.
É possível ter paralelismo sem concorrência?
Não de forma útil, no sentido aqui discutido. A execução paralela requer unidades de trabalho independentes para distribuir, e definir essas unidades é o que significa concorrência. O paralelismo ao nível do hardware, como o SIMD, é uma exceção — ele paraleliza um único fluxo de instruções sobre os dados sem qualquer estrutura concorrente no programa.
O GIL do Python ainda existe?
Sim, na compilação predefinida. Desde a versão 3.13, o CPython também inclui uma compilação opcional com threads livres e o GIL desativado, e a PEP 779 elevou essa compilação ao estatuto de suporte oficial, com vista à versão 3.14. A compilação padrão ainda o inclui, pelo que, a menos que tenha instalado deliberadamente um interpretador com threads livres, o GIL está presente.
O async é o mesmo que multithreading?
Não. A concorrência async utiliza um único thread com comutação cooperativa em pontos «await» explícitos, pelo que apenas uma parte do seu código é executada de cada vez e as comutações ocorrem apenas onde as definiu. O multithreading utiliza várias threads do sistema operativo com comutação preemptiva que pode ocorrer em qualquer lugar. A assíncrona é mais fácil de compreender; as threads podem alcançar um paralelismo real sempre que o tempo de execução o permitir.
Quantas solicitações simultâneas devo fazer?
Menos do que pensa. Comece com cerca de 10, meça as solicitações concluídas por segundo e aumente até que esse número deixe de subir. O limite máximo é normalmente a tolerância do servidor de destino, em vez da capacidade da sua máquina, e, para além desse ponto, a concorrência adicional produz erros em vez de rendimento.
A simultaneidade torna o meu código mais rápido?
Apenas se estiver à espera de algo. Para tarefas limitadas pela E/S, os ganhos são significativos. Para tarefas limitadas pela CPU num único núcleo, a simultaneidade torna as coisas ligeiramente mais lentas devido à sobrecarga da comutação — é necessário paralelismo, o que significa múltiplos núcleos e um ambiente de execução capaz de os utilizar.
Qual é a diferença entre multiprocessamento e multithreading?
As threads partilham memória dentro de um único processo, o que torna a comunicação económica e o estado partilhado perigoso. Os processos têm memória separada, o que os torna seguros e torna a comunicação dispendiosa. Na compilação padrão do Python, os processos são a forma de obter paralelismo real para tarefas limitadas pela CPU; os threads proporcionam concorrência para tarefas limitadas pela E/S.
Preciso de proxies para executar pedidos simultâneos?
Não necessariamente. São necessários quando a concorrência torna a sua presença suficientemente visível para que um destino limite a taxa ou bloqueie o endereço de onde provém. No caso de uma API com uma quota generosa ou de um site que tenha permissão para rastrear, a concorrência por si só é suficiente. No caso de um site que imponha limites por endereço, a distribuição torna-se a restrição — e isso é uma questão à parte da correção do seu código.
Conclusão
Vale a pena ter esta distinção em mente, porque transforma uma pergunta vaga — «como é que faço isto mais rápido?» — numa pergunta específica com uma resposta verificável: estou à espera ou estou a processar?
Se estiveres à espera, precisas de concorrência, e precisas dela na forma que a tua linguagem de programação disponibilizar. O ganho é significativo, normalmente não implica custos adicionais em hardware e está disponível em todos os ambientes de execução mais comuns. Se estiver a processar dados, a concorrência por si só não lhe servirá de nada e precisará de paralelismo genuíno — processos, threads de trabalho, goroutines distribuídas pelos núcleos ou, no caso do Python, possivelmente um interpretador com threads livres, com as suas próprias vantagens e desvantagens a ponderar.
A maioria dos programas reais envolve ambas as situações, em fases, e a ordem em que ocorrem é mais importante do que a escolha. Resolva o gargalo dominante, volte a medir e espere que a resposta tenha mudado. Um pipeline que estava 80% limitado pela rede passa a estar 96% limitado pela análise no momento em que se resolve o problema da rede, e a segunda otimização é um trabalho completamente diferente da primeira.