paralelismo: python e twisted

parent-childExecutar algumas tarefas em paralelo no python, usando threads, pode ser problemático, especialmente quando há I/O envolvido.

Apesar do interpretador do python usar as threads nativas do sistema operacional (na maioria dos *nix, pthreads), ele não é 100% thread-safe. Isso não é um bug, mas sim um recurso usado para evitar travamento ou computação incorreta de valores.

Do texto original:

The Python interpreter is not fully thread safe. In order to support multi-threaded Python programs, there’s a global lock, called the global interpreter lock or GIL, that must be held by the current thread before it can safely access Python objects. Without the lock, even the simplest operations could cause problems in a multi-threaded program: for example, when two threads simultaneously increment the reference count of the same object, the reference count could end up being incremented only once instead of twice.

Therefore, the rule exists that only the thread that has acquired the global interpreter lock may operate on Python objects or call Python/C API functions. In order to support multi-threaded Python programs, the interpreter regularly releases and reacquires the lock — by default, every 100 bytecode instructions (this can be changed with sys.setcheckinterval()). The lock is also released and reacquired around potentially blocking I/O operations like reading or writing a file, so that other threads can run while the thread that requests the I/O is waiting for the I/O operation to complete.

Do outro lado, temos o Twisted. Para mim, o Twisted é uma super biblioteca de I/O, com todos os recursos necessários para tratar tarefas de modo assíncrono (non-blocking), considerando ainda aquelas que bloqueiam a execução por determinado tempo (blocking).

Internamente, o Twisted mantém um thread pool para executar as tarefas síncronas, como por exemplo executar um INSERT em banco de dados. Para tal, existem as funções threads.deferToThread e reactor.callInThread.

Porém, se considerarmos o GIL, mencionado acima, temos como resultado um belo problema: enquanto uma tarefa que necessita I/O síncrono está sendo executada em uma thread, o interpretador do python fica bloqueado naquela operação e todo o resto fica parado. Isso causa uma perda de desempenho sem tamanho, e diversos outros efeitos colaterais (como time-out em sockets, etc).

Solucionar o problema de paralelismo no python não é tão complicado, mas também não é tão simples. Existem dois módulos que conheço, que fazem isso: pyprocessing e multiprocessing.

Ambos usam uma API similar à do módulo threading, mas ao invés de threads, criam processos usando fork(), que por sua vez, executam outro interpretador do python e se livram dos efeitos do GIL em um único processo. Mas, quando usados em conjunto com o Twisted, necessitam diversas adaptações para a comunicação entre os processos pai e filhos, pois essa comunicação é feita através de pipes.

Com tudo isso junto, o GIL começa a se tornar um problema e a coisa toda já se parece com uma grande confusão, que para muitos já parece não ter solução decente. Em suma, é o seguinte: escrever programas que necessitam parelelismo em python exige usar pyprocessing ou multiprocessing, mas quando o programa é inteiro assíncrono usando Twisted, tudo fica complicado.

Isso obviamente aconteceu comigo em um sistema relativamente grande, que precisava executar a classificação de alguns dados em paralelo, e quando usava o thread pool do Twisted, o processo inteiro ficava lento devido à grande quantidade de I/O para ler e gravar arquivos no disco. Era o GIL me atrapalhando.

Para solucionar esse problema, escrevi um módulo que usa a função spawnProcess do próprio Twisted, e automaticamente trata do pipe entre os processos pai e filho. Ainda, escrevi um protocolo de comunicação entre eles que permite transmitir e receber dados entre os processos de modo transparente, de maneira assíncrona.

Ainda, esse módulo possui uma classe que cria um pool de processos (não threads) para onde é possível despachar dados para serem processados em paralelo, e aguardar pelo resultado em um deferred, seguindo todo o padrão do Twisted.

Depois de executar todos os testes necessários e deixar o código estável, implementei isso no meu sistema e resolvi todo o problema do paralelismo de maneira simples e elegante, sem nenhuma gambiarra nem esquisitisse.

Denominado Twisted-Parallels, o módulo foi liberado sob a GPL v2 e está disponível no Google Code, com alguns exemplos de utilização.

Anúncios

5 Comentários on “paralelismo: python e twisted”

  1. tecepe disse:

    melhor desenho ever!

  2. Oi Alexandre !
    Cara, eu não consigo entender muito de Twisted. Vejo falando sobre este FW de várias formas.
    Afinal, o que podemos fazer, qual o intuito do Twisted ?

    E quanto ao parelelismo e threads, você não congitou ainda utilizar a biblioteca http://www.parallelpython.com/ ? Me pareceu muito boa…

    Abraço !

    • alef disse:

      Olá Sérgio,

      O Twisted é uma biblioteca que provê vários mecanismos para lidar com arquivos e sockets (file descriptors) de maneira assíncrona. Assim, sua aplicação irá receber “eventos” quando houver atividade nesses file descriptors. Você registra os eventos que deseja receber, e atribui funções a eles – os tais callbacks.

      No Twisted, as funções que enviam dados pelos file descriptors, retornam objetos do tipo Deferred. Quando você faz uma query em um banco de dados, por exemplo um MySQL, essa função retorna um Deferred. O Deferred permite que você atribua um callback, e assim que o resultado da query estiver disponível, seu callback será executado e o resultado será passado para ele como argumento.

      Exemplo:
      d = db.runQuery(“select * from asd”)
      d.addCallback(show_result)

      def show_result(rs):
      print “resultado do select:”, rs

      Dessa maneira, um único processo pode lidar com centenas de milhares de conexões “ao mesmo tempo”. É uma das melhores maneiras de construir servidores e clientes de rede. Ao contrário do modelo de servidore baseado em thread, os servidores assíncronos consomem menos recursos do sistema operacional, e suportam uma quantidade muito maior de file descriptors – ou, conexões ativas.

      • Sérgio disse:

        Cara, muuuito obrigado !
        Explicação suscinta que me fez entender o que faz o twisted !
        hehehe

        Realmente eu tinha dificuldade em entender quao a função dele exatamente. E agora entendi o que você fez também com o cyclone.
        Bem legal heim !

        vlw…


Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s