Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Concurrent Python - Melhorando a performance co...

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers. →

Concurrent Python - Melhorando a performance com concorrencia

Avatar for jhonatan

jhonatan

May 26, 2018

Other Decks in Programming

Transcript

  1. Process Forking / Spawning • Fork: No unix, um processo

    pode ser “clonado” (Fork), e o processo clonado se torna um processo filho, esse processo é idêntico ao pai, mas a partir do momento do fork pode tomar um rumo bem diferente do processo pai (as variáveis e descritores de arquivos são copiados e perdem referência em relação ao pai) • Spawn: No windows um processo pode criar outro inteiramente novo, injetar as variáveis presentes e agrupá-lo ao processo que o criou o processo “pai” (Spawning), esse modelo é bem mais lento que o fork, pois depende uma abstração maior para ser gerado
  2. Processos X Threads • Naturalmente processos não compartilham informações entre

    sí e correm paralelamente e independentes, pois rodam em espaços de memória diferentes • Threads compartilham informações, pois rodam no mesmo espaço de memória • Processos filhos devem ser devidamente controlados, para não virarem zumbis, e devido a sua natureza de serem clones, gerar um clone dentro de um loop sem delimitações adequadas, o processo filho pode continuar o loop gerando subclones • Threads devem rodar em modo thread-safe, deve se tomar muito cuidado com corrupção de dados • Em ambos modelos, quando necessitam de conexão a outros sistemas como RDBM’s, só podem fazê-lo se os mesmos forem compatíveis (multitenancy)
  3. Thread Scheduling (context switching) • Quantum: pedacinhos de tempo que

    são cedidos a um thread, para que ele seja executado, até que tenha de ceder processamento para o próximo na fila de prioridade • Priority: os threads são executados em fila e cada SO se utiliza de um algoritmo diferente para ordená-los • State: estado atual do thread, “runnable”, “waiting”, etc • Quanta: “clock ticks”, ciclos de processamento, cada thread é dado uma quantidade de quantas entre 2, 6 ou 12 e cada quanta (ciclo), dura 1 ms no linux e entre 12 e 15 ms no windows • Performance: Tendo tudo isso em vista poderemos entender algumas implicações de performance
  4. Shared Memory • É possível compartilhar dados entre processos através

    de memória compartilhada • Ao compartilhar memória é muito importante tomar cuidado com a concorrência (escritas simultâneas), para não corromper os dados • É comum usar locks (travas), para impedir escrita concorrente nesses dados • Locks podem se tornar um gargalo de performance se mal usadas, pois travam também os processos concorrentes que podem ficar esperando a liberação das travas para acessar os dados
  5. Locking • Podemos então constatar que em ambos modelos é

    possível ter acesso a dados em comum • Devemos lembrar que em processos diferentes, mesmo compartilhando a memória, um processo filho não consegue em tese ficar parado esperando que outros processos enviem dados, a não ser que use mensageria via stream, sockets, key-value storages ou uma implementação do shared memory que trave a leitura enquanto outro processo estiver escrevendo • Já os threads tem acesso total as variáveis um do outro e usam sistema de travas, onde um thread entra em modo de espera até que outro mande um sinal de liberação, isso é comumente chamado de locks e semaphores
  6. Implementação Python • Python tem implementações de baixo e alto

    nível • Implementa tanto processos quanto threads • Possui mecanismos tanto de shared memory quanto de mensageria • Simples e robusto, trata todas as complexidades de fork inerentes aos SO’s • Mecanismos de locks e semaphores extremamente simplificados • Nas implementações de médio e alto nível é possível usar pools de processos ou threads
  7. Implementação Python • threading.Thread: classe que cria um thread, recebe

    um callable para executar em um thread separado • queue.Queue: classe “thread safe” que é usada como uma fila de dados compartilhados entre threads (pode ser também usada como fila comum em aplicações python) • multiprocessing.Process: classe de mesma interface de threading.Thread que faz fork de processos e executa apenas um callable específico e retorna para o processo pai • multiprocessing.Pool: classe que cria um pool de processos, em sua assinatura recebe o número de processos a serem criados. Essa classe possui métodos que distribuem processos usando “round-robin”
  8. Implementação Python • multiprocessing.Queue: fila com implementação shared memory, que

    permite compartilhamento entre processos “forkados”, gera locks • multiprocessing.pool.ThreadPool: Gera um pool de threads com um callback de excução. • concurrent.futures.ThreadPoolExecutor: implementação high level que utiliza a arquitetura de executor e utiliza um pool de threads • concurrent.futures.ProcessPoolExecutor: implementação high level que utiliza a arquitetura de executor e utiliza um pool de processos
  9. Implementação Python, Abstração mais básica • As classes Thread e

    Process recebem um callable em seu construtor • Ambas possuem métodos de controle de fluxo, start inicia o processo e não bloqueia, run roda o próximo e bloqueia até que seja concluído • A classe Queue é uma estrutura de fila FIFO que é também Thread-Safe
  10. Implementação Python, Pools • Pool é para processos e ThreadPool

    para threads • São criados processos ou threads na quantidade especificada que ficam abertos aguardando callables para serem executados • Ganho de performance, pois não precisa abrir uma thread a cada callable, executa na thread já aberta
  11. Implementação Python, Concurrent Futures • A ideia é que ao

    mandar o executor executar um callable, ele retorne um objeto Future que contém informações sobre o processo a ser executado • O método result retornará ao processo/thread pai o valor retornado pelo callable, se houver uma exceção no callable, result vai lançá-lo • Interface única
  12. Amostras de performance Ambiente de testes: • Ubuntu Linux 16.04

    x64 • Intel(R) Core(TM) i7-3612QM CPU @ 2.10GHz • 8 gb DDR3 • HD SSD 250 gb • Python 3.6 • PyPy3 / Python 3.5
  13. Threads com fila Tempo de execução: ~1.276 seg. Fato interesante:

    se usarmos 1 thread ao invés de 3 como no exemplo, o tempo cai para ~0.686
  14. Thread Safety • Estruturas de dados que possuem tratamento de

    acessos simultâneos entre múltiplos threads • Estruturas que evitam deadlocks (trava recursiva, onde uma thread espera pela outra de forma recursiva) • Técnicas como mutual exclusion (trava escrita e leitura enquanto uma thread escreve), Objetos imutáveis, operações atômicas, Re-entrance (código que gera cópias dos valores e manipula apenas individualmente)
  15. Global Interpreter Lock (GIL) • O interpretador Cpython gera opcodes

    em tempo real quando está sendo executado • Os opcodes por ele gerados não são thread-safe, portanto, não é possível que diversos threads modifiquem os opcodes ao mesmo tempo • O Cpython então implementa o GIL, uma trava que impede escritas e leituras simultâneas aos opcode gerados em runtime, gerando performance muito ruim em determinados casos • Ainda não se descobriu uma forma de remover o GIL sem degradar a performance do Cpython em processos single threaded
  16. Conheça o PyPy • Implementação python que utiliza um compilador

    JIT que gera bytecodes que podem ser executados diretamente pela máquina • Além do PyPy ser um interpretador muito mais rápido, os bytecodes gerados são thread-safe, ou seja, não existe GIL no PyPy • Isso permite uma programação multi-thread muito mais fluida • O PyPy já vem com o modo stackless e greenlets embutidos
  17. Python stackless • O conceito stackless se refere à uma

    implementação do que é chamada de microthreads • Microthreads são threads que não são controladas pelo SO e sim pelo próprio interpretador, ou seja, não ocorre context switching e dispensa uso direto dos mecanismos de locking • A comunicação entre “stacklets” ocorre via mensageria (envio de mensagens), ou seja, uma implementação pub-sub que evita acessos simultâneos aos dados
  18. Async I/O • Event Loop • Selectable Module (Waiting I/O

    completion), select, poll, kqueue, kevent • Coroutines / Generators • async / await