Quantcast
Channel: Planet Python
Viewing all articles
Browse latest Browse all 22462

Diego Garcia: Threads em Python? é claro!

$
0
0

Muito provavelmente você já deve ter ouvido a mesma lenda que me foi contada quando estava apreendendo python:

Python não é bom com threads

Isso não é totalmente mentira, mas a questão é, threads só não serão efetivas com o python, se você não usar da forma correta.

O temível GIL

O CPython (interpretador padrão do python) possui o Global Interpreter Lock, também conhecido como GIL. Um mecanismo (presente também em outras linguagens como o Ruby) responsável por prevenir o uso de paralelismo, fazendo com que apenas uma thread seja executada no interpretador por vez. Isso faz com que, mesmo criando inúmeras threads, o desempenho de uma rotina singlethread sejá melhor do que o desempenho de uma rotina multithread, já que internamente, apenas uma thread estará fazendo uso da cpu por vez (mesmo em um ambiente multicore).

Existem vários beneficios e malefícios relacionados ao GIL e talvez esse seja o motivo pelo qual muitas pessoas acreditam que em python, threads não são efetivas.

Porém, o que nem todos sabem é que para operações de I/O (network/socket e escrita em arquvios) o GILé liberado, ou seja, sempre que uma tarefa de I/O for executada (por exemplo, consultar um servidor externo) o GILé liberado para que outro processo seja executado de forma paralela até essa primeira chamada retornar resultado. Vamos ver isso na prática.

Problema do mundo real: multiplos requests http

Uma situação muito comum no dia a dia de um desenvolvedor é ter que lidar com multiplos requests externos na mesma rotina. Usaremos um exemplo ficticio de uma aplicação de linha de comando que imprimi a cotação do real em relação a algumas moedas estrangeiras. Essas cotações serão recuperadas do site Dolar Hoje e sites similares.

Recuperando as cotações

Recuperaremos as cotações das seguintes modeas: dolar, euro, libra e peso

CURRENCY={'dolar':'http://dolarhoje.com/','euro':'http://eurohoje.com/','libra':'http://librahoje.com/','peso':'http://pesohoje.com/'}

Como esses sites utilizam o mesmo formato, utilizaremos uma regex padrão para processa-los:

DEFAULT_REGEX=r'<input type="text" id="nacional" value="([^"]+)"/>'

Para recuperar essas cotações, faremos uma espécie de web crawler que fara um GET na página e via RegEx será recuperada a informação sobre a cotação monetária. O método para realizar esse request é extremamente simples:

importrefromurllib.requestimporturlopendefexchange_rate(url):response=urlopen(url).read().decode('utf-8')result=re.search(DEFAULT_REGEX,response)ifresult:returnresult.group(1)

Um simples get e decode do conteúdo de uma url através da urlib e a busca do padrão de uma regex através da função search do pacote re nativo do python para lidar com regex.

Utilizei a urllib por ser uma biblioteca nativa do python, porém, para esse tipo de operação (e qualquer outro tipo de request sincrono) recomendo o uso da bibliotéca requests

Execução serial

Para recuperar a cotação de todas as urls listadas no dicionáro CURRENCY de forma serial, basta iterar pelos items (chave, valor) desse dicionário, executando a função exchange_rate para cada um passando a url como parâmetro.

forcurrency,urlinCURRENCY.items():print('{}: R${}'.format(currency,exchange_rate(url)))

Cada iteração desse for só será finalizada após a função exchange_rate processar a url informada, ou seja, o tempo demorado será algo em torno do tempo do primeiro request vezes o número de items do dicionário.

Execução multithread

Para executar essa mesma rotina mas de forma paralela, utilizaremos a forma mais moderna de se trabalhar com concorrencia em python, o módulo concurrent.futures. Esse módulo permite através de um Executor executar tarefas assincronas através de threads ou sub processos.

O módulo concurrent.futures está disponivel apartir da versão 3.2 do python, porém, possui o backport futures compatível com python 2.7

O módulo concurrent.futures possuí 2 principais componentes:

  • Executor: Interface que possui métodos para executar rotinas de forma assincrona.
  • Future: Interface que encapsula a execução assincrona de uma rotina.

Para executarmos nossa função exchange_rate de forma assincrona deveremos executar o método submit do executor (em nosso caso, uma instância de ThreadPoolExecutor). Esse método aceita como parâmetro a função que será executada de forma assincrona e seus *args e **kwargs, no nosso caso devemos passar a função exchange_rate e a url. O método submit retorna uma instância de Future que encapsulara a execução assincrona da rotina.

Em nosso problema, precisamos iniciar todos os requests e aguardar até que todos sejam concluídos, para que isso seja possível basta criar futures dessas rotinas e processar as que forem concluídas.

fromconcurrent.futuresimportas_completed,ThreadPoolExecutorwithThreadPoolExecutor(max_workers=len(CURRENCY))asexecutor:waits={executor.submit(exchange_rate,url):currencyforcurrency,urlinCURRENCY.items()}forfutureinas_completed(waits):currency=waits[future]print('{}: R${}'.format(currency,future.result()))

O gerador as_completed do módulo concurrent.futures retorna as futures que forem concluídas na ordem em que forem concluídas. Após a future estar concluída, basta recuperar seu resultado através do método result().

Repare que na criação do executor foi necessário especificar o número de workers que serão utilizados para executar as rotinas assincronas, porém, na versão 3.5 do python esse parâmetro não é mais obrigatório e caso ele seja omitido, o python assume o número de processadores na máquina

Comparando a execução

Apesar de o mesmo numero de requests externos estarem sendo executados em ambos os casos, a execução serial executa um request por vez, enquanto que a execução multithread executa todos os requests de uma só vez, de forma paralela (sem intervenção do GIL) diminuindo assim o tempo de execução da aplicação de forma exponencial

  • Execução serial
$ time python multi_requests.py
libra: R$5,78
dolar: R$4,00
euro: R$4,47
peso: R$0,27

real    0m3.476s
user    0m0.129s
sys     0m0.004s
  • Execução multithread
$ time python multi_requests.py threads
dolar: R$4,00
euro: R$4,47
libra: R$5,78
peso: R$0,27

real    0m1.433s
user    0m0.122s
sys     0m0.025s

Note que a execução serial demorou em torno de 3.47 segundos contra 1.43 segundos da excução multithread. Essa diferença tende a crescer de acordo com a quantidade de requests feitos.

Veja o código completo desse exemplo neste gist.

Conclusão

Em resumo, toda vez que alguém enche a boca para me dizer "python não é bom com threads" essa é a minha reação:

(╯°□°)╯︵ ┻━┻

Referências
Launching parallel tasks
Understanding the Python GIL


Viewing all articles
Browse latest Browse all 22462

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>