Quem fala mal de threads são tipicamente programadoras de sistemas, que tem em mente casos de uso que o típico programador de aplicações nunca vai encontrar na vida.[...] Em 99% dos casos de uso que o programador de aplicações vai encontrar, o modelo simples de gerar um monte de threads e coletar os resultados em uma fila é tudo que se precisa saber.
Michele Simionato, profundo pensador do Python. Do post de Michele Simionato, "Threads, processes and concurrency in Python: some thoughts" (_Threads, processos e concorrência em Python: algumas reflexões_), resumido assim: "Removendo exageros sobre a (não-)revolução dos múltiplos núcleos e alguns comentários sensatos (oxalá) sobre threads e outras formas de concorrência."
Este capítulo se concentra nas classes do concurrent.futures.Executor
,
que encapsulam o modelo de "gerar um monte de threads independentes e coletar os resultados em uma fila" descrito por Michele Simionato.
Executores concorrentes tornam o uso desse modelo quase trivial,
não apenas com threads mas também com processos—úteis para tarefas de processamento intensivo em CPU.
Também introduzo aqui o conceito de
futures—objetos que representam a execução assíncrona de uma operação, similares às promises do Javascript.
Essa ideia básica é a fundação de concurrent.futures
bem como do pacote asyncio
, assunto do [async_ch].
Renomeei este capítulo de "Concorrência com futures" para "Executores concorrentes", porque os executores são o recurso de alto nível mais importante tratado aqui. Futures são objetos de baixo nível, tratados na Onde estão os futures?, mas quase invisíveis no resto do capítulo.
Todos os exemplos de clientes HTTP agora usam a nova biblioteca HTTPX, que oferece APIs síncronas e assíncronas.
A configuração para os experimentos na Download com exibição do progresso e tratamento de erro ficou mais simples,
graças ao servidor de múltiplas threads adicionado ao pacote http.server
no Python 3.7.
Antes, a biblioteca padrão oferecia apenas o BaseHttpServer
de thread única,
que não era adequado para experiências com clientes concorrentes,
então na primeira edição desse livro precisei usar um servidor externo.
A Iniciando processos com concurrent.futures agora demonstra como um executor simplifica o código que vimos na [code_for_multicore_prime_sec].
Por fim, movi a maior parte da teoria para o novo [concurrency_models_ch].
A concorrência é essencial para uma comunicação eficiente via rede: em vez de esperar de braços cruzados por respostas de máquinas remotas, a aplicação deveria fazer alguma outra coisa até a resposta chegar.[1]
Para demonstrar com código, escrevi três programas simples que baixam da web imagens de 20 bandeiras de países.
O primeiro, flags.py, roda sequencialmente:
ele só requisita a imagem seguinte quando a anterior foi baixada e salva localmente.
Os outros dois scripts fazem downloads concorrentes:
eles requisitam várias imagens quase ao mesmo tempo, e as salvam conforme chegam.
O script flags_threadpool.py usa o pacote concurrent.futures
,
enquanto flags_asyncio.py usa asyncio
.
O Exemplo 1 mostra o resultado da execução dos três scripts, três vezes cada um.
Os scripts baixam imagens de fluentpython.com, que usa uma CDN (Content Delivery Network, Rede de Fornecimento de Conteúdo), então você pode notar os resultados mais lentos nas primeiras passagens. Os resultados no Exemplo 1 foram obtidos após várias execuções, então o cache da CDN estava carregado.
$ python3 flags.py
BD BR CD CN DE EG ET FR ID IN IR JP MX NG PH PK RU TR US VN (1)
20 flags downloaded in 7.26s (2)
$ python3 flags.py
BD BR CD CN DE EG ET FR ID IN IR JP MX NG PH PK RU TR US VN
20 flags downloaded in 7.20s
$ python3 flags.py
BD BR CD CN DE EG ET FR ID IN IR JP MX NG PH PK RU TR US VN
20 flags downloaded in 7.09s
$ python3 flags_threadpool.py
DE BD CN JP ID EG NG BR RU CD IR MX US PH FR PK VN IN ET TR
20 flags downloaded in 1.37s (3)
$ python3 flags_threadpool.py
EG BR FR IN BD JP DE RU PK PH CD MX ID US NG TR CN VN ET IR
20 flags downloaded in 1.60s
$ python3 flags_threadpool.py
BD DE EG CN ID RU IN VN ET MX FR CD NG US JP TR PK BR IR PH
20 flags downloaded in 1.22s
$ python3 flags_asyncio.py (4)
BD BR IN ID TR DE CN US IR PK PH FR RU NG VN ET MX EG JP CD
20 flags downloaded in 1.36s
$ python3 flags_asyncio.py
RU CN BR IN FR BD TR EG VN IR PH CD ET ID NG DE JP PK MX US
20 flags downloaded in 1.27s
$ python3 flags_asyncio.py
RU IN ID DE BR VN PK MX US IR ET EG NG BD FR CN JP PH CD TR (5)
20 flags downloaded in 1.42s
-
A saída de cada execução começa com os códigos dos países de cada bandeira a medida que as imagens são baixadas, e termina com uma mensagem mostrando o tempo decorrido.
-
flags.py precisou em média de 7,18s para baixar 20 imagens.
-
A média para flags_threadpool.py foi 1,40s.
-
Já flags_asyncio.py, obteve um tempo médio de 1,35s.
-
Observe a ordem do códigos de país: nos scripts concorrentes, as imagens foram baixadas em um ordem diferente a cada vez.
A diferença de desempenho entre os scripts concorrentes não é significativa, mas ambos são mais de cinco vezes mais rápidos que o script sequencial—e isto apenas para a pequena tarefa de baixar 20 arquivos, cada um com uns poucos kilobytes. Se você escalar a tarefa para centenas de downloads, os scripts concorrentes podem superar o código sequencial por um fator de 20 ou mais.
Warning
|
Ao testar clientes HTTP concorrentes usando servidores web públicos, você pode inadvertidamente lançar um ataque de negação de serviço (DoS, Denial of Service attack), ou se tornar suspeito de estar tentando um ataque.
No caso do Exemplo 1 não há problema, pois aqueles scripts estão codificados para realizar apenas 20 requisições.
Mais adiante nesse capítulo usaremos o pacote |
Vamos agora estudar as implementações de dois dos scripts testados no Exemplo 1: flags.py e flags_threadpool.py. Vou deixar o terceiro, flags_asyncio.py, para o [async_ch], mas queria demonstrar os três juntos para fazer duas observações:
-
Independente dos elementos de concorrência que você use—threads ou corrotinas—haverá um ganho enorme de desempenho sobre código sequencial em operações de E/S de rede, se o script for escrito corretamente.
-
Para clientes HTTP que podem controlar quantas requisições eles fazem, não há diferenças significativas de desempenho entre threads e corrotinas.[2]
Vamos ver o código.
O Exemplo 2 contém a implementação de flags.py, o primeiro script que rodamos no Exemplo 1. Não é muito interessante, mas vamos reutilizar a maior parte do código e das configurações para implementar os scripts concorrentes, então ele merece alguma atenção.
Note
|
Por clareza, não há qualquer tipo de tratamento de erro no Exemplo 2. Vamos lidar come exceções mais tarde, mas aqui quero me concentrar na estrutura básica do código, para facilitar a comparação deste script com os scripts que usam concorrência. |
link:code/20-executors/getflags/flags.py[role=include]
-
Importa a biblioteca
httpx
. Ela não é parte da biblioteca padrão. Assim, por convenção, a importação aparece após os módulos da biblioteca padrão e uma linha em branco. -
Lista do código de país ISO 3166 para os 20 países mais populosos, em ordem decrescente de população.
-
O diretório com as imagens das bandeiras.[3]
-
Diretório local onde as imagens são salvas.
-
Salva os bytes de
img
parafilename
noDEST_DIR
. -
Dado um código de país, constrói a URL e baixa a imagem, retornando o conteúdo binário da resposta.
-
É uma boa prática adicionar um timeout razoável para operações de rede, para evitar ficar bloqueado sem motivo por vários minutos.
-
Por default, o HTTPX não segue redirecionamentos.[4]
-
Não há tratamento de erros nesse script, mas esse método lança uma exceção se o status do HTTP não está na faixa 2XX—algo mutio recomendado para evitar falhas silenciosas.
-
download_many
é a função chave para comparar com as implementações concorrentes. -
Percorre a lista de códigos de país em ordem alfabética, para facilitar a confirmação de que a ordem é preservada na saída; retorna o número de códigos de país baixados.
-
Mostra um código de país por vez na mesma linha, para vermos o progresso a cada download. O argumento
end=' '
substitui a costumeira quebra no final de cada linha escrita com um espaço, assim todos os códigos de país aparecem progressivamente na mesma linha. O argumentoflush=True
é necessário porque, por default, a saída do Python usa um buffer de linha, o que significa que o Python só mostraria os caracteres enviados após uma quebra de linha. -
main
precisa ser chamada com a função que fará os downloads; dessa forma podemos usarmain
como uma função de biblioteca com outras implementações dedownload_many
nos exemplos dethreadpool
eascyncio
. -
Cria o
DEST_DIR
se necessário; não acusa erro se o diretório existir. -
Mede e apresenta o tempo decorrido após rodar a função
downloader
. -
Chama
main
com a funçãodownload_many
.
Tip
|
A biblioteca HTTPX é inspirada no pacote pythônico
requests,
mas foi desenvolvida sobre bases mais modernas.
Especialmente, HTTPX tem APIs síncronas e assíncronas,
então podemos usá-la em todos os exemplos de clientes HTTP nesse capítulo e no próximo.
A biblioteca padrão do Python contém o módulo |
Não há mesmo nada de novo em flags.py.
Ele serve de base para comparação com outros scripts, e o usei como uma biblioteca, para evitar código redundante ao implementar aqueles scripts.
Vamos ver agora uma reimplementação usando concurrent.futures
.
Os principais recursos do pacote concurrent.futures
são as classes ThreadPoolExecutor
e ProcessPoolExecutor
, que implementam uma API para submissão de callables ("chamáveis") para execução em diferentes threads ou processos, respectivamente.
As classes gerenciam de forma transparente um grupo de threads ou processos de trabalho, e filas para distribuição de tarefas e coleta de resultados.
Mas a interface é de um nível muito alto, e não precisamos saber nada sobre qualquer desses detalhes para um caso de uso simples como nossos downloads de bandeiras.
O Exemplo 3 mostra a forma mais fácil de implementar os downloads de forma concorrente, usando o método ThreadPoolExecutor.map
.
futures.ThreadPoolExecutor
link:code/20-executors/getflags/flags_threadpool.py[role=include]
-
Reutiliza algumas funções do módulo
flags
(Exemplo 2). -
Função para baixar uma única imagem; isso é o que cada thread de trabalho vai executar.
-
Instancia o
ThreadPoolExecutor
como um gerenciador de contexto; o métodoexecutor.__exit__
vai chamarexecutor.shutdown(wait=True)
, que vai bloquear até que todas as threads terminem de rodar. -
O método
map
é similar aomap
embutido, exceto que a funçãodownload_one
será chamada de forma concorrente por múltiplas threads; ele retorna um gerador que você pode iterar para recuperar o valor retornado por cada chamada da função—nesse caso, cada chamada adownload_one
vai retornar um código de país. -
Retorna o número de resultados obtidos. Se alguma das chamadas das threads levantar uma exceção, aquela exceção será levantada aqui quando a chamada implícita
next()
, dentro do construtor delist
, tentar recuperar o valor de retorno correspondente, no iterador retornado porexecutor.map
. -
Chama a função
main
do móduloflags
, passando a versão concorrente dedownload_many
.
Observe que a função download_one
do Exemplo 3 é essencialmente o corpo do loop for
na função download_many
do Exemplo 2. Essa é uma refatoração comum quando se está escrevendo código concorrente: transformar o corpo de um loop for
sequencial em uma função a ser chamada de modo concorrente.
Tip
|
O Exemplo 3 é muito curto porque pude reutilizar a maior parte das funções do script sequencial flags.py.
Uma das melhores características do |
O construtor de ThreadPoolExecutor
recebe muitos argumentos além dos mostrados aqui, mas o primeiro e mais importante é max_workers
, definindo o número máximo de threads de trabalho a serem executadas.
Quando max_workers
é None
(o default),
ThreadPoolExecutor
decide seu valor usando, desde o Python 3.8, a seguinte expressão:
max_workers = min(32, os.cpu_count() + 4)
A justificativa é apresentada na documentação de ThreadPoolExecutor
:
Esse valor default conserva pelo menos 5 threads de trabalho para tarefas de E/S. Ele utiliza no máximo 32 núcleos da CPU para tarefas de processamento, o quê libera a GIL. E ele evita usar recursos muitos grandes implicitamente em máquinas com muitos núcleos.
ThreadPoolExecutor
agora também reutiliza threads de trabalho inativas antes iniciar [novas] threads de trabalho demax_workers
.
Concluindo: o valor default calculado de max_workers
é razoável, e ThreadPoolExecutor
evita iniciar novas threads de trabalho desnecessariamente.
Entender a lógica por trás de max_workers
pode ajudar a decidir quando e como estabelecer o valor em seu código.
A biblioteca se chama concurrency.futures, mas não há qualquer future à vista no Exemplo 3, então você pode estar se perguntando onde estão eles. A próxima seção explica isso.
Os futures (literalmente "futuros") são componentes centrais de concurrent.futures
e de asyncio
, mas como usuários dessas bibliotecas, raramente os vemos.
O Exemplo 3 depende de futures por trás do palco,
mas o código apresentado não lida diretamente com objetos dessa classe.
Essa seção apresenta uma visão geral dos futures, com um exemplo mostrando-os em ação.
Desde o Python 3.4, há duas classes chamadas Future
na biblioteca padrão:
concurrent.futures.Future
e asyncio.Future
.
Elas tem o mesmo propósito:
uma instância de qualquer das classes Future
representa um processamento adiado,
que pode ou não ter sido completado.
Isso é algo similar à classe Deferred
no Twisted,
a classe Future
no Tornado, e objetos Promise
no Javascript moderno.
Os futures encapsulam operações pendentes de forma que possamos colocá-los em filas, verificar se terminaram, e recuperar resultados (ou exceções) quando eles ficam disponíveis.
Uma coisa importante de saber sobre futures é eu e você, não devemos criá-los:
eles são feitos para serem instanciados exclusivamente pelo framework de concorrência,
seja ela a concurrent.futures
ou a asyncio
.
O motivo é que um Future
representa algo que será executado em algum momento,
portanto precisa ser agendado para rodar, e quem agenda tarefas é o framework.
Especificamente, instâncias concurrent.futures.Future
são criadas apenas como resultado da submissão de um objeto invocável (callable)
para execução a uma subclasse de concurrent.futures.Executor
.
Por exemplo, o método Executor.submit()
recebe um invocável, agenda sua execução e retorna um Future
.
O código da aplicação não deve mudar o estado de um future: o framework de concorrência muda o estado de um future quando o processamento que ele representa termina, e não temos como controlar quando isso acontece.
Ambos os tipos de Future
tem um método .done()
não-bloqueante, que retorna um Boolean informando se o invocável encapsulado por aquele future
foi ou não executado.
Entretanto, em vez de perguntar repetidamente se um future terminou, o código cliente em geral pede para ser notificado.
Por isso as duas classes Future
tem um método .add_done_callback()
:
você passa a ele um invocável e aquele invocável será invocado com o future como único argumento, quando o future tiver terminado.
Observe que aquele invocável de callback será invocado na mesma thread ou processo de trabalho que rodou a função encapsulada no future.
Há também um método .result()
, que funciona igual nas duas classes quando a execução do future termina:
ele retorna o resultado do invocável, ou relança qualquer exceção que possa ter aparecido quando o invocável foi executado.
Entretanto, quando o future não terminou, o comportamento do método result
é bem diferente entre os dois sabores de Future
.
Em uma instância de concurrency.futures.Future
,
invocar f.result()
vai bloquear a thread que chamou até o resultado ficar pronto.
Um argumento timeout
opcional pode ser passado, e se o future não tiver terminado após aquele tempo, o método result
gera um TimeoutError
.
O método asyncio.Future.result
não suporta um timeout, e await
é a forma preferencial de obter o resultado de futures no asyncio
—mas await
não funciona com instâncias de concurrency.futures.Future
.
Várias funções em ambas as bibliotecas retornam futures; outras os usam em sua implementação de uma forma transparente para o usuário.
Um exemplo desse último caso é o Executor.map
, que vimos no Exemplo 3:
ele retorna um iterador no qual __next__
chama o método result
de cada future, então recebemos os resultados dos futures, mas não os futures em si.
Para ver uma experiência prática com os futures, podemos reescrever o Exemplo 3 para usar a função concurrent.futures.as_completed
, que recebe um iterável de futures e retorna um iterador que
entrega futures quando cada um encerra sua execução.
Usar futures.as_completed
exige mudanças apenas na função download_many
.
A chamada ao executor.map
, de alto nível, é substituída por dois loops for
:
um para criar e agendar os futures, o outro para recuperar seus resultados.
Já que estamos aqui, vamos acrescentar algumas chamadas a print
para mostrar cada future antes e depois do término de sua execução.
O Exemplo 4 mostra o código da nova função download_many
.
O código de download_many
aumentou de 5 para 17 linhas,
mas agora podemos inspecionar os misteriosos futures.
As outras funções são idênticas as do Exemplo 3.
executor.map
por executor.submit
e futures.as_completed
na função download_many
link:code/20-executors/getflags/flags_threadpool_futures.py[role=include]
-
Para essa demonstração, usa apenas os cinco países mais populosos.
-
Configura
max_workers
para3
, para podermos ver os futures pendentes na saída. -
Itera pelos códigos de país em ordem alfabética, para deixar claro que os resultados vão aparecer fora de ordem.
-
executor.submit
agenda o invocável a ser executado, e retorna umfuture
representando essa operação pendente. -
Armazena cada
future
, para podermos recuperá-los mais tarde comas_completed
. -
Mostra uma mensagem com o código do país e seu respectivo
future
. -
as_completed
entrega futures conforme eles terminam. -
Recupera o resultado desse
future
. -
Mostra o
future
e seu resultado.
Observe que a chamada a future.result()
nunca bloqueará a thread nesse exemplo, pois future
está vindo de as_completed
. O
Exemplo 5 mostra a saída de uma execução do Exemplo 4.
$ python3 flags_threadpool_futures.py
Scheduled for BR: <Future at 0x100791518 state=running> (1)
Scheduled for CN: <Future at 0x100791710 state=running>
Scheduled for ID: <Future at 0x100791a90 state=running>
Scheduled for IN: <Future at 0x101807080 state=pending> (2)
Scheduled for US: <Future at 0x101807128 state=pending>
CN <Future at 0x100791710 state=finished returned str> result: 'CN' (3)
BR ID <Future at 0x100791518 state=finished returned str> result: 'BR' (4)
<Future at 0x100791a90 state=finished returned str> result: 'ID'
IN <Future at 0x101807080 state=finished returned str> result: 'IN'
US <Future at 0x101807128 state=finished returned str> result: 'US'
5 downloads in 0.70s
-
Os futures são agendados em ordem alfabética; o
repr()
de umfuture
mostra seu estado: os três primeiros estãorunning
, pois há três threads de trabalho. -
Os dois últimos
futures
estãopending
; esperando pelas threads de trabalho. -
O primeiro
CN
aqui é a saída dedownload_one
em uma thread de trabalho; o resto da linha é a saída dedownload_many
. -
Aqui, duas threads retornam códigos antes que
download_many
na thread principal possa mostrar o resultado da primeira thread.
Tip
|
Recomendo experimentar com flags_threadpool_futures.py.
Se você o rodar várias vezes, vai ver a ordem dos resultados variar.
Aumentar |
Vimos duas variantes do script de download usando concurrent.futures
: uma no Exemplo 3 com ThreadPoolExecutor.map
e uma no Exemplo 4 com futures.as_completed
.
Se você está curioso sobre o código de flags_asyncio.py, pode espiar o [flags_asyncio_ex] no [async_ch], onde ele é explicado.
Agora vamos dar uma olhada rápida em um modo simples de desviar da GIL para tarefas de uso intensivo de CPU, usando concurrent.futures
.
A página de documentação de concurrent.futures
tem por subtítulo "Iniciando tarefas em paralelo." O pacote permite computação paralela em máquinas multi-núcleo porque suporta a distribuição de trabalho entre múltiplos processos Python usando a classe
ProcessPoolExecutor
.
Ambas as classes, ProcessPoolExecutor
e ThreadPoolExecutor
implementam a interface Executor
, então é fácil mudar de uma solução baseada em threads para uma baseada em processos usando concurrent.futures
.
Não há nenhuma vantagem em usar um ProcessPoolExecutor
no exemplo de download de bandeiras ou em qualquer tarefa concentrada em E/S.
É fácil comprovar isso; apenas modifique as seguintes linhas no Exemplo 3:
def download_many(cc_list: list[str]) -> int:
with futures.ThreadPoolExecutor() as executor:
para:
def download_many(cc_list: list[str]) -> int:
with futures.ProcessPoolExecutor() as executor:
O construtor de ProcessPoolExecutor
também tem um parâmetro max_workers
, que por default é None
. Nesse caso, o executor limita o número de processos de trabalho ao número resultante de uma chamada a os.cpu_count()
.
Processos usam mais memória e demoram mais para iniciar que threads, então o real valor de of ProcessPoolExecutor
é em tarefas de uso intensivo da CPU.
Vamos voltar ao exemplo de teste de verificação de números primos de[naive_multiprocessing_sec], e reescrevê-lo com concurrent.futures
.
Na [code_for_multicore_prime_sec], estudamos procs.py, um script que verificava se alguns números grandes eram primos usando multiprocessing
.
No Exemplo 6 resolvemos o mesmo problema com o programa proc_pool.py, usando um ProcessPoolExecutor
. Do primeiro import
até a chamada a main()
no final, procs.py tem 43 linhas de código não-vazias, e proc_pool.py tem 31—28% mais curto.
ProcessPoolExecutor
link:code/20-executors/primes/proc_pool.py[role=include]
-
Não há necessidade de importar
multiprocessing
,SimpleQueue
etc.;concurrent.futures
esconde tudo isso. -
A tupla
PrimeResult
e a funçãocheck
são as mesmas que vimos em procs.py, mas não precisamos mais das filas nem da funçãoworker
. -
Em vez de decidirmos por nós mesmos quantos processos de trabalho serão usados se um argumento não for passado na linha de comando, atribuímos
None
aworkers
e deixamos oProcessPoolExecutor
decidir. -
Aqui criei o
ProcessPoolExecutor
antes do blocowith
em ➐, para poder mostrar o número real de processos na próxima linha. -
max_workers
é um atributo de instância não-documentado de umProcessPoolExecutor
. Decidi usá-lo para mostrar o número de processos de trabalho criados quando a variávelworkers
éNone
. O Mypy corretamente reclama quando eu acesso esse atributo, então coloquei o comentáriotype: ignore
para silenciar a reclamação. -
Ordena os números a serem verificados em ordem descendente. Isso vai mostrar a diferença no comportamento de proc_pool.py quando comparado a procs.py. Veja a explicação após esse exemplo.
-
Usa o
executor
como um gerenciador de contexto. -
A chamada a
executor.map
retorna as instâncias dePrimeResult
retornadas porcheck
na mesma ordem dos argumentosnumbers
.
Se você rodar o Exemplo 6, verá os resultados aparecente em ordem rigorosamente descendente, como mostrado no Exemplo 7.
Por outro lado, a ordem da saída de procs.py (mostrado em [proc_based_solution]) é severamente influenciado pela dificuldade em verificar se cada número é ou não primo.
Por exemplo, procs.py mostra o resultado para 7777777777777777 próximo ao topo, pois ele tem um divisor pequeno, 7, então is_prime
rapidamente determina que ele não é primo.
Já o de 7777777536340681 is 881917092, então is_prime
vai demorar muito mais para determinar que esse é um número composto,
e ainda mais para descobrir que 7777777777777753 é primo—assim, ambos esses números aparecem próximo do final da saída de procs.py.
Ao rodar proc_pool.py, podemos observar não apenas a ordem descendente dos resultados, mas também que o programa parece emperrar após mostrar o resultado para 9999999999999999.
$ ./proc_pool.py
Checking 20 numbers with 12 processes:
9999999999999999 0.000024s # (1)
9999999999999917 P 9.500677s # (2)
7777777777777777 0.000022s # (3)
7777777777777753 P 8.976933s
7777777536340681 8.896149s
6666667141414921 8.537621s
6666666666666719 P 8.548641s
6666666666666666 0.000002s
5555555555555555 0.000017s
5555555555555503 P 8.214086s
5555553133149889 8.067247s
4444444488888889 7.546234s
4444444444444444 0.000002s
4444444444444423 P 7.622370s
3333335652092209 6.724649s
3333333333333333 0.000018s
3333333333333301 P 6.655039s
299593572317531 P 2.072723s
142702110479723 P 1.461840s
2 P 0.000001s
Total time: 9.65s
-
Essa linha aparece muito rápido.
-
Essa linha demora mais de 9,5s para aparecer.
-
Todas as linhas restantes aparecem quase imediatamente.
Aqui está o motivo para aquele comportamento de proc_pool.py:
-
Como mencionado antes,
executor.map(check, numbers)
retorna o resultado na mesma ordem em quenumbers
é enviado. -
Por default, proc_pool.py usa um número de processos de trabalho igual ao número de CPUs—isso é o que
ProcessPoolExecutor
faz quandomax_workers
éNone
. Nesse laptop são então 12 processos. -
Como estamos submetendo
numbers
em ordem descendente, o primeiro é 9999999999999999; com 9 como divisor, ele retorna rapidamente. -
O segundo número é 9999999999999917, o maior número primo na amostra. Ele vai demorar mais que todos os outros para verificar.
-
Enquanto isso, os 11 processos restantes estarão verificando outros números, que são ou primos ou compostos com fatores grandes ou compostos com fatores muito pequenos.
-
Quando o processo de trabalho encarregado de 9999999999999917 finalmente determina que ele é primo, todos os outros processos já completaram suas últimas tarefas, então os resultados aparecem logo depois.
Note
|
Apesar do progresso de proc_pool.py não ser tão visível quanto o de procs.py, o tempo total de execução, para o mesmo número de processo de trabalho e de núcleos de CPU, é praticamente idêntico, como retratado em [procs_x_time_fig]. |
Entender como programas concorrentes se comportam não é um processo direto, então aqui está um segundo experimento que pode ajudar a visualizar o funcionamento de
Executor.map
.
Vamos investigar Executor.map
, agora usando um ThreadPoolExecutor
com três threads de trabalho rodando cinco chamáveis que retornam mensagens marcadas com data/hora. O código está no Exemplo 8, o resultado no Exemplo 9.
ThreadPoolExecutor
link:code/20-executors/demo_executor_map.py[role=include]
-
Essa função exibe o momento da execução no formato
[HH:MM:SS]
e os argumentos recebidos. -
loiter
não faz nada além mostrar uma mensagem quanto inicia, dormir porn
segundos, e mostrar uma mensagem quando termina; são usadas tabulações para indentar as mensagens de acordo com o valor den
. -
loiter
retornan * 10
, então podemos ver como coletar resultados. -
Cria um
ThreadPoolExecutor
com três threads. -
Submete cinco tarefas para o
executor
. Já que há apenas três threads, apenas três daquelas tarefas vão iniciar imediatamente: a chamadasloiter(0)
,loiter(1)
, eloiter(2)
; essa é uma chamada não-bloqueante. -
Mostra imediatamente o
results
da invocação deexecutor.map
: é um gerador, como se vê na saída no Exemplo 9. -
A chamada
enumerate
no loopfor
vai invocar implicitamentenext(results)
, que por sua vez vai invocarf.result()
no future (interno)f
, representando a primeira chamada,loiter(0)
. O métodoresult
vai bloquear a thread até que o future termine, portanto cada iteração nesse loop vai esperar até que o próximo resultado esteja disponível.
Encorajo você a rodar o Exemplo 8 e ver o resultado sendo atualizado de forma incremental. Quando for fazer isso, mexa no argumento max_workers
do ThreadPoolExecutor
e com a função range
, que produz os argumentos para a chamada a executor.map
—ou os substitua por listas com valores escolhidos, para criar intervalos diferentes.
$ python3 demo_executor_map.py
[15:56:50] Script starting. (1)
[15:56:50] loiter(0): doing nothing for 0s... (2)
[15:56:50] loiter(0): done.
[15:56:50] loiter(1): doing nothing for 1s... (3)
[15:56:50] loiter(2): doing nothing for 2s...
[15:56:50] results: <generator object result_iterator at 0x106517168> (4)
[15:56:50] loiter(3): doing nothing for 3s... (5)
[15:56:50] Waiting for individual results:
[15:56:50] result 0: 0 (6)
[15:56:51] loiter(1): done. (7)
[15:56:51] loiter(4): doing nothing for 4s...
[15:56:51] result 1: 10 (8)
[15:56:52] loiter(2): done. (9)
[15:56:52] result 2: 20
[15:56:53] loiter(3): done.
[15:56:53] result 3: 30
[15:56:55] loiter(4): done. (10)
[15:56:55] result 4: 40
-
Essa execução começou em 15:56:50.
-
A primeira thread executa
loiter(0)
, então vai dormir por 0s e retornar antes mesmo da segunda thread ter chance de começar, mas YMMV.[5] -
loiter(1)
eloiter(2)
começam imediatamente (como o pool de threads tem três threads de trabalho, é possível rodar três funções de forma concorrente). -
Isso mostra que o
results
retornado porexecutor.map
é um gerador: nada até aqui é bloqueante, independente do número de tarefas e do valor demax_workers
. -
Como
loiter(0)
terminou, a primeira thread de trabalho está disponível para iniciar a quarta thread paraloiter(3)
. -
Aqui é ponto a execução pode ser bloqueada, dependendo dos parâmetros passados nas chamadas a
loiter
: o método__next__
do geradorresults
precisa esperar até o primeiro future estar completo. Neste caso, ele não vai bloquear porque a chamada aloiter(0)
terminou antes desse loop iniciar. Observe que tudo até aqui aconteceu dentro do mesmo segundo: 15:56:50. -
loiter(1)
termina um segundo depois, em 15:56:51. A thread está livre para iniciarloiter(4)
. -
O resultado de
loiter(1)
é exibido:10
. Agora o loopfor
ficará bloqueado, esperando o resultado deloiter(2)
. -
O padrão se repete:
loiter(2)
terminou, seu resultado é exibido; o mesmo ocorre comloiter(3)
. -
Há um intervalo de 2s até
loiter(4)
terminar, porque ela começou em 15:56:51 e não fez nada por 4s.
A função Executor.map
é fácil de usar,
mas muitas vezes é preferível obter os resultados assim que estejam prontos, independente da ordem em que foram submetidos.
Para fazer isso, precisamos de uma combinação do método Executor.submit
e da função futures.as_completed
como vimos no Exemplo 4. Vamos voltar a essa técnica na Usando futures.as_completed.
Tip
|
A combinação de |
Na próxima seção vamos retomar os exemplos de download de bandeiras com novos requerimentos que vão nos obrigar a iterar sobre os resultados de futures.as_completed
em vez de usar executor.map
.
Como mencionado, os scripts em Downloads concorrentes da web não tem tratamento de erros, para torná-los mais fáceis de ler e para comparar a estrutura das três abordagens: sequencial, com threads e assíncrona.
Para testar o tratamento de uma variedade de condições de erro, criei os exemplos flags2
:
- flags2_common.py
-
Este módulo contém as funções e configurações comuns, usadas por todos os exemplos
flags2
, incluindo a funçãomain
, que cuida da interpretação da linha de comando, da medição de tempo e de mostrar os resultados. Isso é código de apoio, sem relevância direta para o assunto desse capítulo, então não vou incluir o código-fonte aqui, mas você pode vê-lo no fluentpython/example-code-2e repositório: 20-executors/getflags/flags2_common.py. - flags2_sequential.py
-
Um cliente HTTP sequencial com tratamento de erro correto e a exibição de uma barra de progresso. Sua função
download_one
também é usada porflags2_threadpool.py
. - flags2_threadpool.py
-
Cliente HTTP concorrente, baseado em
futures.ThreadPoolExecutor
, para demonstrar o tratamento de erros e a integração da barra de progresso. - flags2_asyncio.py
-
Mesma funcionalidade do exemplo anterior, mas implementado com
asyncio
ehttpx
. Isso será tratado na [flags2_asyncio_sec], no [async_ch].
Warning
|
Tenha cuidado ao testar clientes concorrentes
Ao testar clientes HTTP concorrentes em servidores web públicos, você pode gerar muitas requisições por segundo, e é assim que ataques de negação de serviço (DoS, denial-of-service) são feitos. Controle cuidadosamente seus clientes quando for usar servidores públicos. Para testar, configure um servidor HTTP local. Veja o Configurando os servidores de teste para instruções. |
A característica mais visível dos exemplos flags2
é sua barra de progresso animada em modo texto, implementada com o pacote tqdm. Publiquei um vídeo de 108s no YouTube mostrando a barra de progresso e comparando a velocidade dos três scripts flags2
. No vídeo, começo com o download sequencial, mas interrompo a execução após 32s. O script demoraria mais de 5 minutos para acessar 676 URLs e baixar 194 bandeiras. Então rodo o script usando threads e o que usa asyncio
, três vezes cada um, e todas as vezes eles completam a tarefa em 6s ou menos (isto é mais de 60 vezes mais rápido). A Figura 1 mostra duas capturas de tela: durante e após a execução de flags2_threadpool.py.
O exemplo de uso mais simples do tqdm aparece em um .gif animado, no README.md do projeto. Se você digitar o código abaixo no console do Python após instalar o pacote tqdm, uma barra de progresso animada aparecerá no lugar onde está o comentário:
>>> import time
>>> from tqdm import tqdm
>>> for i in tqdm(range(1000)):
... time.sleep(.01)
...
>>> # -> progress bar will appear here <-
Além do efeito elegante, o tqdm
também é conceitualmente interessante:
ele consome qualquer iterável, e produz um iterador que, enquanto é consumido, mostra a barra de progresso e estima o tempo restante para completar todas as iterações. Para calcular aquela estimativa, o tqdm
precisa receber um iterável que tenha um len
, ou receber adicionalmente o argumento total=
com o número esperado de itens. Integrar o tqdm
com nossos exemplos flags2
proporciona um oportunidade de observar mais profundamente o funcionamento real dos scripts concorrentes, pois nos obriga a usar as funções futures.as_completed
e asyncio.as_completed
, para permitir que o tqdm
mostre o progresso conforme cada future
é termina sua execução.
A outra característica dos exemplos flags2
é a interface de linha de comando.
Todos os três scripts aceitam as mesmas opções,
e você pode vê-las rodando qualquer um deles com a opção -h
.
O Exemplo 10 mostra o texto de ajuda.
$ python3 flags2_threadpool.py -h
usage: flags2_threadpool.py [-h] [-a] [-e] [-l N] [-m CONCURRENT] [-s LABEL]
[-v]
[CC [CC ...]]
Download flags for country codes. Default: top 20 countries by population.
positional arguments:
CC country code or 1st letter (eg. B for BA...BZ)
optional arguments:
-h, --help show this help message and exit
-a, --all get all available flags (AD to ZW)
-e, --every get flags for every possible code (AA...ZZ)
-l N, --limit N limit to N first codes
-m CONCURRENT, --max_req CONCURRENT
maximum concurrent requests (default=30)
-s LABEL, --server LABEL
Server to hit; one of DELAY, ERROR, LOCAL, REMOTE
(default=LOCAL)
-v, --verbose output detailed progress info
Todos os argumentos são opcionais. Mas o -s/--server
é essencial para os testes:
ele permite escolher qual servidor HTTP e qual porta serão usados no teste.
Passe um desses parâmetros (insensíveis a maiúsculas/minúsculas) para determinar onde o script vai buscar as bandeiras:
LOCAL
-
Usa
http://localhost:8000/flags
; esse é o default. Você deve configurar um servidor HTTP local, respondendo na porta 8000. Veja as instruções na nota a seguir. REMOTE
-
Usa
http://fluentpython.com/data/flags
; este é meu site público, hospedado em um servidor compartilhado. Por favor, não o martele com requisições concorrentes excessivas. O domínio fluentpython.com é gerenciado pela CDN (Content Delivery Network, Rede de Fornecimento de Conteúdo) da Cloudflare, então você pode notar que os primeiros downloads são mais lentos, mas ficam mais rápidos conforme o cache da CDN é carregado. DELAY
-
Usa
http://localhost:8001/flags
; um servidor atrasando as respostas HTTP deve responder na porta 8001. Escrevi o slow_server.py para facilitar o experimento. Ele está no diretório 20-futures/getflags/ do repositório de código do Python Fluente. Veja as instruções na nota a seguir. ERROR
-
Usa
http://localhost:8002/flags
; um servidor devolvendo alguns erros HTTP deve responder na porta 8002. Instruções a seguir.
Note
|
Configurando os servidores de teste
Se você não tem um servidor HTTP local para testes, escrevi instruções de configuração usando apenas Python ≥ 3.9 (nenhuma biblioteca externa) em 20-executors/getflags/README.adoc no fluentpython/example-code-2e repositório. Em resumo, o README.adoc descreve como usar:
|
Por default, cada script flags2*.py irá baixar as bandeiras dos 20 países mais populosos do servidor LOCAL
(http://localhost:8000/flags
), usando um número default de conexões concorrentes, que varia de script para script.
O Exemplo 11 mostra uma execução padrão do script flags2_sequential.py usando as configurações default.
Para rodá-lo, você precisa de um servidor local, como explicado em Tenha cuidado ao testar clientes concorrentes.
site LOCAL
, as 20 bandeiras dos países mais populosos, 1 conexão concorrente$ python3 flags2_sequential.py
LOCAL site: http://localhost:8000/flags
Searching for 20 flags: from BD to VN
1 concurrent connection will be used.
--------------------
20 flags downloaded.
Elapsed time: 0.10s
Você pode selecionar as bandeiras a serem baixadas de várias formas. O Exemplo 12 mostra como baixar todas as bandeiras com códigos de país começando pelas letras A, B ou C.
DELAY
todas as bandeiras com prefixos de códigos de país A, B ou C$ python3 flags2_threadpool.py -s DELAY a b c
DELAY site: http://localhost:8001/flags
Searching for 78 flags: from AA to CZ
30 concurrent connections will be used.
--------------------
43 flags downloaded.
35 not found.
Elapsed time: 1.72s
Independente de como os códigos de país são selecionados, o número de bandeiras a serem obtidas pode ser limitado com a opção -l/--limit
. O Exemplo 13 demonstra como fazer exatamente 100 requisições, combinando a opção -a
para obter todas as bandeiras com -l 100
.
-al 100
) do servidor ERROR
, usando 100 requisições concorrentes (-m 100
)$ python3 flags2_asyncio.py -s ERROR -al 100 -m 100
ERROR site: http://localhost:8002/flags
Searching for 100 flags: from AD to LK
100 concurrent connections will be used.
--------------------
73 flags downloaded.
27 errors.
Elapsed time: 0.64s
Essa é a interface de usuário dos exemplos flags2
. Vamos ver como eles estão implementados.
A estratégia comum em todos os três exemplos para lidar com erros HTTP é que erros 404 (not found) são tratados pela função encarregada de baixar um único arquivo (download_one
). Qualquer outra exceção propaga para ser tratada pela função download_many
ou pela corrotina supervisor
—no exemplo de asyncio
.
Vamos novamente começar estudando o código sequencial, que é mais fácil de compreender—e muito reutilizado pelo script com um pool de threads. O Exemplo 14 mostra as funções que efetivamente fazer os downloads nos scripts flags2_sequential.py e flags2_threadpool.py.
link:code/20-executors/getflags/flags2_sequential.py[role=include]
-
Importa a biblioteca de exibição de barra de progresso
tqdm
, e diz ao Mypy para não checá-la.[6] -
Importa algumas funções e um
Enum
do móduloflags2_common
. -
Dispara um
HTTPStatusError
se o código de status do HTTP não está emrange(200, 300)
. -
download_one
trata oHTTPStatusError
, especificamente para tratar o código HTTP 404… -
…mudando seu
status
local paraDownloadStatus.NOT_FOUND
;DownloadStatus
é umEnum
importado de flags2_common.py. -
Qualquer outra exceção de
HTTPStatusError
é re-emitida e propagada para quem chamou a função. -
Se a opção de linha de comando
-v/--verbose
está vigente, o código do país e a mensagem de status são exibidos; é assim que você verá o progresso no modoverbose
.
O Exemplo 15 lista a versão sequencial da função download_many
. O código é simples, mas vale a pena estudar para compará-lo com as versões concorrentes que veremos a seguir. Se concentre em como ele informa o progresso, trata erros e conta os downloads.
download_many
link:code/20-executors/getflags/flags2_sequential.py[role=include]
-
Este
Counter
vai registrar os diferentes resultados possíveis dos downloads:DownloadStatus.OK
,DownloadStatus.NOT_FOUND
, ouDownloadStatus.ERROR
. -
cc_iter
mantém a lista de códigos de país recebidos como argumentos, em ordem alfabética. -
Se não estamos rodando em modo
verbose
,cc_iter
é passado para otqdm
, que retorna um iterador que produz os itens emcc_iter
enquanto também anima a barra de progresso. -
Faz chamadas sucessivas a
download_one
. -
As exceções do código de status HTTP ocorridas em
get_flag
e não tratadas pordownload_one
são tratadas aqui. -
Outras exceções referentes à rede são tratadas aqui. Qualquer outra exceção vai interromper o script, porque a função
flags2_common.main
, que chamadownload_many
, não tem nenhumtry/except
. -
Sai do loop se o usuário pressionar Ctrl-C.
-
Se nenhuma exceção saiu de
download_one
, limpa a mensagem de erro. -
Se houve um erro, muda o
status
local de acordo com o erro. -
Incrementa o contador para aquele
status
. -
Se no modo
verbose
, mostra a mensagem de erro para o código de país atual, se houver. -
Retorna
counter
para quemain
possa mostrar os números no relatório final.
Agora vamos estudar flags2_threadpool.py, o exemplo de pool de threads refatorado.
Para integrar a barra de progresso do tqdm e tratar os erros a cada requisição, o script flags2_threadpool.py usa o futures.ThreadPoolExecutor
com a função, já vista anteriormente, futures.as_completed
. O Exemplo 16 é a listagem completa de flags2_threadpool.py. Apenas a função download_many
é implementada; as outras funções são reutilizadas de flags2_common.py e flags2_sequential.py.
link:code/20-executors/getflags/flags2_threadpool.py[role=include]
-
Reutiliza
download_one
deflags2_sequential
(Exemplo 14). -
Se a opção de linha de comando
-m/--max_req
não é passada, este será o número máximo de requisições concorrentes, implementado como o tamanho do poll de threads; o número real pode ser menor se o número de bandeiras a serem baixadas for menor. -
MAX_CONCUR_REQ
limita o número máximo de requisições concorrentes independente do número de bandeiras a serem baixadas ou da opção de linha de comando-m/--max_req
. É uma medida de segurança, para evitar iniciar threads demais, com seu uso significativo de memória. -
Cria o
executor
commax_workers
determinado porconcur_req
, calculado pela funçãomain
como o menor de:MAX_CONCUR_REQ
, o tamanho decc_list
, ou o valor da opção de linha de comando-m/--max_req
. Isso evita criar mais threads que o necessário. -
Este
dict
vai mapear cada instância deFuture
—representando um download—com o respectivo código de país, para exibição de erros. -
Itera sobre a lista de códigos de país em ordem alfabética. A ordem dos resultados vai depender, mais do que de qualquer outra coisa, do tempo das respostas HTTP; mas se o tamanho do pool de threads (dado por
concur_req
) for muito menor quelen(cc_list)
, você poderá ver os downloads aparecendo em ordem alfabética. -
Cada chamada a
executor.submit
agenda a execução de uma invocável e retorna uma instância deFuture
. O primeiro argumento é a invocável, o restante são os argumentos que ela receberá. -
Armazena o
future
e o código de país nodict
. -
futures.as_completed
retorna um iterador que produz futures conforme cada tarefa é completada. -
Se não estiver no modo
verbose
, passa o resultado deas_completed
com a funçãotqdm
, para mostrar a barra de progresso; comodone_iter
não temlen
, precisamos informar otqdm
qual o número de itens esperado com o argumentototal=
, para que ele possa estimar o trabalho restante. -
Itera sobre os futures conforme eles vão terminando.
-
Chamar o método
result
em um future retorna ou o valor retornado pela invocável ou dispara qualquer exceção que tenha sido capturada quando a invocável foi executada. Esse método pode bloquear quem chama, esperando por uma resolução. Mas não nesse exemplo, porqueas_completed
só retorna futures que terminaram sua execução. -
Trata exceções em potencial; o resto dessa função é idêntica à função
download_many
no Exemplo 15), exceto pela observação a seguir. -
Para dar contexto à mensagem de erro, recupera o código de país do
to_do_map
, usando ofuture
atual como chave. Isso não era necessário na versão sequencial, pois estávamos iterando sobre a lista de códigos de país, então sabíamos qual era occ
atual; aqui estamos iterando sobre futures.
Tip
|
O Exemplo 16 usa um idioma que é muito útil com |
As threads do Python são bastante adequadas a aplicações de uso intensivo de E/S, e o pacote concurrent.futures
as torna relativamente simples de implementar em certos casos de uso. Com ProcessPoolExecutor
você também pode resolver problemas de uso intensivo de CPU em múltiplos núcleos—se o processamento for "embaraçosamente paralelo". Isso encerra nossa introdução básica a concurrent.futures
.
Nós começamos o capítulo comparando dois clientes HTTP concorrentes com um sequencial, demonstrando que as soluções concorrentes mostram um ganho significativo de desempenho sobre o script sequencial.
Após estudar o primeiro exemplo, baseado no concurrent.futures
, olhamos mais de perto os objetos future, instâncias de concurrent.futures.Future
ou de asyncio.Future
, enfatizando as semelhanças entre essas classes (suas diferenças serão examinadas no [async_ch]). Vimos como criar futures chamando Executor.submit
, e como iterar sobre futures que terminaram sua execução com concurrent.futures.as_completed
.
Então discutimos o uso de múltiplos processos com a classe concurrent.futures.ProcessPoolExecutor
, para evitar a GIL e usar múltiplos núcleos de CPU, simplificando o verificador de números primos multi-núcleo que vimos antes no [concurrency_models_ch].
Na seção seguinte vimos como funciona a concurrent.futures.ThreadPoolExecutor
, com um exemplo didático, iniciando tarefas que apenas não faziam nada por alguns segundos, exceto exibir seu status e a hora naquele instante.
Nós então voltamos para os exemplos de download de bandeiras. Melhorar aqueles exemplos com uma barra de progresso e tratamento de erro adequado nos ajudou a explorar melhor a função geradora future.as_completed
mostrando um modelo comum: armazenar futures em um dict
para anexar a eles informação adicional quando são submetidos, para podermos usar aquela informação quando o future sai do iterador as_completed
.
O pacote concurrent.futures
foi uma contribuição de Brian Quinlan, que o apresentou em uma palestra sensacional intitulada "The Future Is Soon!" (EN), na PyCon Australia 2010. A palestra de Quinlan não tinha slides; ele mostra o que a biblioteca faz digitando código diretamente no console do Python. Como exemplo motivador, a apresentação inclui um pequeno vídeo com o cartunista/programador do XKCD, Randall Munroe, executando um ataque de negação de serviço (DoS) não-intencional contra o Google Maps, para criar um mapa colorido de tempos de locomoção pela cidade. A introdução formal à biblioteca é a PEP 3148 - futures
- execute computations asynchronously (`futures` - executar processamento assíncrono) (EN). Na PEP, Quinlan escreveu que a biblioteca concurrent.futures
foi "muito influenciada pelo pacote java.util.concurrent
do Java."
Para recursos adicionais falando do concurrent.futures
,
por favor consulte o [concurrency_models_ch].
Todas as referências que tratam de threading
e multiprocessing
do Python na [concurrency_further_threads_procs_sec] também tratam do concurrent.futures
.
Evitando Threads
Concorrência: um dos tópicos mais difíceis na ciência da computação (normalmente é melhor evitá-lo).
David Beazley, educador Python e cientista louco—Slide #9 do tutorial "A Curious Course on Coroutines and Concurrency" ("Um Curioso Curso sobre Corrotinas e Concorrência") (EN), apresentado na PyCon 2009.
Eu concordo com as citações aparentemente contraditórias de David Beazley e Michele Simionato no início desse capítulo.
Assisti um curso de graduação sobre concorrência.
Tudo o que vimos foi programação de threads POSIX.
O que aprendi: que não quero gerenciar threads e travas pessoalmente, pela mesma razão que não quero gerenciar a alocação e desalocação de memória pessoalmente.
Essas tarefas são melhor desempenhadas por programadores de sistemas, que tem o conhecimento, a inclinação e o tempo para fazê-las direito—ou assim esperamos.
Sou pago para desenvolver aplicações, não sistemas operacionais. Não preciso desse controle fino de threads, travas, malloc
e free
—veja "Alocação dinâmica de memória em C".
Por isso acho o pacote concurrent.futures
interessante: ele trata threads, processos, e filas como infraestrutura, algo a seu serviço, não algo que você precisa controlar diretamente. Claro, ele foi projetado pensando em tarefas simples, os assim chamado problemas embaraçosamente paralelos—ao contrário de sistemas operacionais ou servidores de banco de dados, como aponta Simionato naquela citação.
Para problemas de concorrência "não embaraçosos", threads e travas também não são a solução. Ao nível do sistema operacional, as threads nunca vão desaparecer. Mas todas as linguagens de programação que achei empolgantes nos últimos muitos anos fornecem abstrações de alto nível para concorrência, como demonstra o excelente livro de Paul Butcher, Seven Concurrency Models in Seven Weeks (Sete Modelos de Concorrência em Sete Semanas) (EN). Go, Elixir, e Clojure estão entre elas. Erlang—a linguagem de implementação do Elixir—é um exemplo claro de uma linguagem projetada desde o início pensando em concorrência. Erlang não me excita por uma razão simples: acho sua sintaxe feia. O Python me acostumou mal.
José Valim, antes um dos contribuidores centrais do Ruby on Rails, projetou o Elixir com uma sintaxe moderna e agradável. Como Lisp e Clojure, o Elixir implementa macros sintáticas. Isso é uma faca de dois gumes. Macros sintáticas permitem criar DSLs poderosas, mas a proliferação de sub-linguagens pode levar a bases de código incompatíveis e à fragmentação da comunidade. O Lisp se afogou em um mar de macros, cada empresa e grupo de desenvolvedores Lisp usando seu próprio dialeto arcano. A padronização em torno do Common Lisp resultou em uma linguagem inchada. Espero que José Valim inspire a comunidade do Elixir a evitar um destino semelhante. Até agora, o cenário parece bom. O invólucro de bancos de dados e gerador de queries Ecto é muito agradável de usar: um grande exemplo do uso de macros para criar uma DSL—sigla de Domain-Specific Language, Linguagem de Domínio Específico—flexível mas amigável, para interagir com bancos de dados relacionais e não-relacionais.
Como o Elixir, o Go é uma linguagem moderna com ideias novas. Mas, em alguns aspectos, é uma linguagem conservadora, comparada ao Elixir. O Go não tem macros, e sua sintaxe é mais simples que a do Python. O Go não suporta herança ou sobrecarga de operadores, e oferece menos oportunidades para metaprogramação que o Python. Essas limitações são consideradas benéficas. Elas levam a comportamentos e desempenho mais previsíveis. Isso é uma grande vantagem em ambientes de missão crítica altamente concorrentes, onde o Go pretende substituir C++, Java e Python.
Enquanto Elixir e Go são competidores diretos no espaço da alta concorrência, seus projetos e filosofias atraem públicos diferentes. Ambos tem boas chances de prosperar. Mas historicamente, as linguagens mais conservadoras tendem a atrair mais programadores.
follow_redirects=True
não é necessário nesse exemplo, mas eu queria destacar essa importante diferença entre HTTPX e requests. Além disso, definir follow_redirects=True
nesse exemplo me dá a flexibilidade de armazenar os arquivos de imagem em outro lugar no futuro. Acho sensata a configuração default do HTTPX, follow_redirects=False
, pois redirecionamentos inesperados podem mascarar requisições desnecessárias e complicar o diagnóstico de erro.
loiter(1)
começar antes de loiter(0)
terminar, especialmente porque sleep
sempre libera a GIL, então o Python pode mudar para outra thread mesmo se você dormir por 0s.
tqdm
. Tudo bem. O mundo não vai acabar por causa disso. Obrigado, Guido, pela tipagem opcional!