Comparando threads em Python vs. Java Bruno Menegola1 1 Instituto de Informática – Universidade Federal do Rio Grande do Sul (UFRGS) Caixa Postal 15.064 – 91.501-970 – Porto Alegre – RS – Brazil [email protected] Abstract. Python é uma linguagem que tem como objetivo criar códigos de alta legibilidade. Java possui um módulo bastante desenvolvido de threads. Esse módulo será comparado ao módulo de threads de Python. Serão apresentados aspectos de sintaxe (criação e manipulação), sincronização e desempenho. Também é discutido as implicações do Global Interpreter Lock no Python, porque ele ainda está presente e possı́veis soluções para o problema de multiprocessamento. 1. Introdução O uso de processamento paralelo em computação já possui uma longa história. Entretanto, nos últimos anos tivemos um barateamento de processadores com mais de um núcleo, o que torna a possibilidade de múltiplos fluxos de execução em quase uma necessidade para alcançar toda a demanda de computação requerida. Porém não só nesses computadores de mesa, ainda temos grandes servidores com inúmeros processadores e uma grande memória. O uso de threads é especialmente eficiente no processamento e no desenvolvimento dos sistemas. Nesse trabalho serão comparados os módulos que permite o uso de threads, em duas linguagens: Python e Java. O módulo de threads em Python foi inspirado diretamente pelo módulo de Java. Até mesmo o nome dos métodos era semelhante em versões iniciais. Além da sintaxe de criação bastante distinta, serão comentados o que existe ou não em cada linguagem e os seus porquês, quais são os problemas existentes em Python comparado a Java, porque problemas de desempenho existem e uma possı́vel solução. Durante todo o trabalho é feita a comparação com Java, porém focando um pouco mais em Python, já que é menos conhecido e divulgado. O texto possui algumas regiões parecidas com um tutorial, mas isso é para que o leitor conheça as possibilidades que o Python provê. Tudo que for apresentado, terá uma comparação com Java, sempre que possı́vel. Na Seção 2 será discutido aspectos de sintaxe (criação e manipulação de threads) e também métodos de sincronização. Na Seção 3 serão apresentados aspectos de performance e porque eles são desta forma. 2. Sintaxe Python e Java foram criados em tempos e, principalmente, com objetivos distintos. Por esse motivo, ambas possuem diferenças gritantes tanto na sintaxe dos recursos implementados. Java prove meios de criar códigos seguros, com tipagem forte e estática, colocando uma caracterı́stica importante de linguagens de programação orientadas a objeto em primeiro plano: o encapsulamento. Python, por sua vez, foi criado para permitir que os desenvolvedores que a utilizassem pudessem criar códigos com alta legibilidade e grande poder, mantendo a idéia de codificar menos e produzir mais. O Python possui algumas “regras de ouro” que foram escritas pelos seus criadores e são aconselhadas a serem seguidas por quem utiliza essa linguagem. Essas regras fazem parte do Zen do Python [Peters 2004] e explicam muitos dos motivos pelos quais a linguagem é o que é. Duas delas estão citadas abaixo: “Simples é melhor que complexo.” “Legibilidade conta.” Mas tratando da comparação de threads, embora o Python tenha sido influenciado fortemente pelo Java [Python.org 2009d] quando o módulo de thread foi definido, a sintaxe utilizada para sua criação e manipulação levam em conta essas regras – entre outros motivos. Possivelmente, Python seja a linguagem em que é necessário o menor esforço para tratar threads. As suas facilidades serão detalhadas a seguir, nas próximas subseções. 2.1. Criação Existem duas formas, em Java, de se criar novas threads: por herança ou interface. A criação por herança, acredito que seria a forma mais natural de criação. Basta saber que existe uma classe que implementa a thread e que você quer criar uma nova que extende essa classe. Entretanto, para resolver o problema de não haver herança múltipla em Java, são utilizadas as interfaces. Dessa forma é possı́vel implementar threads com classes que já herdam elementos de outras. Por outro lado, em Python, também existem duas maneiras de criar threads, porém elas tem propósitos diferentes. Como dito, a forma mais natural de criação é por herança. Em Python, um exemplo simples dessa forma está definido abaixo: 1 2 3 4 5 6 7 8 9 10 11 12 from t h r e a d i n g import Thread from time import s l e e p c l a s s H e l l o ( Thread ) : def run ( s e l f ) : sleep (3) print ' H e l l o World ! thread = Hello () print ' I n i c i a n d o t h r e a d . . . thread . s t a r t () print ' Thread i n i c i a d a . ' ' ' Nesse exemplo, uma classe que extende Thread é definida nas linhas 4-7. A instância criada, quando iniciada, dorme por 3 segundos, imprime uma string na tela e finaliza a thread, bem como o programa principal que estava esperando seu término. 2 Também nesse exemplo já podemos ver como é feita a herança de threads além de alguns métodos principais (run e start). As opções de criação de threads e seus métodos principais serão explicados mais adiante. Comparando com Java, onde eram necessárias interfaces para implementar threads em classes que já herdavam elementos, Python possui herança múltipla e isso não é problema. Basta declarar a lista de classes que deseja-se herdar e pronto. Não vou dar exemplos neste trabalho para não me extender demais com códigos, mas o formato da declaração pode ser visualizado em [Python.org 2009a]. Claro que a inexistência de interfaces pode reduzir o encapsulamento, mas isso também não é o tópico deste trabalho e também não é o objetivo da linguagem. A outra forma de criação de threads é bastante parecida com a forma implementada em C#. Ela permite que qualquer função ou método de instância ou de classe seja transformado em uma thread. Um exemplo simples é exibido abaixo: 1 2 3 4 5 6 7 8 9 10 11 12 13 from t h r e a d i n g import Thread from time import s l e e p class Hello : def f o o ( s e l f ) : sleep (3) print ' H e l l o World ! ' hello = Hello () t h r e a d = Thread ( t a r g e t=h e l l o . f o o ) print ' I n i c i a n d o t h r e a d . . . ' thread . s t a r t () print ' Thread i n i c i a d a . ' Esse código produz o mesmo resultado que o exemplo anterior. Porém, a forma como a thread é instanciada é bastante distinta. Dessa vez, o seu método de execução principal é o método passado como parâmetro na sua criação (linha 10). Essa última forma é um grande avanço em relação a Java Threads. Isso torna extremamente fácil a criação de threads. Entretanto, nem sempre essa forma é recomendada já que pode não ser tão poderosa quanto fazer por herança. Dessa forma também é preciso ter bastante cuidado com seções crı́ticas, pois com a facilidade aumentam-se as chances de o desenvolvedor esquecer desses detalhes. Comparando com Java, a sua classe Thread também possui um parâmetro target, porém deve ser passado, a esse, um objeto que implementa Runnable. Em Python qualquer objeto que possa ser invocado pode ser especificado. Agora que já conhecemos as formas de instanciação. Podemos analisar mais a fundo as opções de criação. abaixo está o cabeçalho do construtor de threads do Python: c l a s s t h r e a d i n g . Thread ( group=None , t a r g e t=None , name=None , a r g s =() , kwargs ={}) 3 As considerações sobre esse construtor estão a seguir: group deve ser None. É um parâmetro reservado para futuras extensões do Python, quando a classe de agrupamento estiver implementada. target é um objeto que possa ser invocado pelo método run(). name é o nome da thread. args e kargs são uma tupla e um dicionário passados como parâmetro de target. Se a subclasse sobrescreve o construtor, ela precisa invocar o construtor da classe base antes de mais nada. Como dito, Python Threads é fortemente inspirado em Java. Tanto é que seus parâmetros são bastante parecidos (veja [Sun Microsystems 2008]). O único que difere é o stackSize que mexe com o tamanho da pilha. Porém isso não é passado como parâmetro no Python, mas pode ser alterado via uma função do módulo threading. Quanto aos agrupamentos, Python ainda não implementa essa parte. Espera-se que no futuro isso seja resolvido. 2.2. Manipulação Abaixo são listados os principais métodos e atributos para manipulação e controle de objetos Thread. Após, isso será comparado com Java Threads. run() – método com o laço principal de execução da thread. Esse é o método que deve ser sobrecarregado ao fazer herança da classe Thread. start() – inicia a atividade da thread. join([timeout]) – bloqueia a thread atual e espera até que a outra termine. Se timeout for especificado, a thread atual fica bloqueada pelo tempo determinado, em segundos (float, pode ser uma fração de segundo), ou até que a thread termine, o que ocorrer antes. Uma thread não pode esperar por si mesma, pois isso geraria um deadlock, e nesse caso é levantado uma exceção. Ergue-se uma exceção também para o caso de um join em uma thread que ainda não foi iniciada. name – o nome da thread. ident – o identificador da thread. is alive() – verifica se a thread está rodando. daemon – valor booleano para verificar e setar a thread como deamon. Precisa ser setada antes de iniciar a thread ou gera uma exceção. Todos os métodos e atributos descritos acima, possuem equivalentes no Java. Entretanto, o Java possui vários outros além desses, que permite mais controle sobre os objetos e fluxo de execução. Abaixo são listados os métodos que não estão presentes no Python e uma breve explicação do porquê ou como produzir um resultado equivalente ou semelhante. Segurança checkAccess() getDefaultUncaughtExceptionHandler() getUncaughtExceptionHandler() setDefaultUncaughtExceptionHandler(...) 4 setUncaughtExceptionHandler(...) O Python não possui um Security Manager ou algo parecido. O propósito dessa linguagem não é ser segura em todos os aspectos como o Java. Um Security Manager dificulta o uso de threads, portanto não foi implementado. Grupos enumerate(...) getThreadGroup() Python não implementa grupos de threads ainda. Qualquer manipulação relacionada a isso é inexistente. Prioridade getPriority() setPriority(...) Por usar threads de núcleo (implementadas usando Pthreads) e nem todos os sistemas operacionais forem iguais na alteração das permissões, Python não permite alterar esse parâmetro. Embora em Unix seja possı́vel alterar o parâmetro nice da thread/processo que é usado no escalonador para determinar as prioridades. Essa função está disponı́vel no módulo os do Python. Estados getState() destroy() (deprecated) resume() (deprecated) stop() (deprecated) suspend() (deprecated) holdsLock(...) Na documentação do Python nada é falado sobre estados das threads. No Java eles são bem definidos e são 6, exatamente. Talvez na representação interna dp Python seja utilizado algo parecido, mas para o usuário isso é transparente. Quanto aos métodos deprecated do Java para interrupção e resumo do fluxo, o Python já não implementou desde o inı́cio pelo motivo que o Java explicita em sua documentação: isso não é seguro; e pois esses métodos tornariam obscuras as formas de término de uma thread, algo que o Python não permite já pela sua filosofia: “Explı́cito é melhor que implı́cito” (do Zen do Python). Quanto ao holdsLock(...), Python não possui monitores (será melhor explicado na Seção 2.3) e não tem como fornecer essa informação. Troca de contexto sleep(...) yield() Ambos métodos não estão presentes na classe Thread do Python. O sleep 5 pode ser usado através da função sleep do módulo time. Terá o mesmo efeito pois as threads do Python são implementadas com Pthreads, como dito, e essa função permite ao escalonador do sistema liberar outra thread. O yield pode ser simulado com um time.sleep(0). 2.3. Sincronização A forma clássica de sincronização em Java é usar monitores. Mascarado pelo modificador syncronized, os monitores organizam o acesso a blocos de comandos pelas threads concorrentes. Embora isso traga grande facilidade e segurança para o programador, nem sempre é possı́vel resolver todos os problemas de forma trivial utilizando esse método. Um exemplo disso é quando é preciso adquirir acesso a uma seção crı́tica em um método e liberá-la em outro. Por essas dificuldades e pela descoberta de novos operadores que facilitariam a sincronização de threads, nas versões mais novas, o Java disponibiliza outras ferramentas: locks, semáforos, barreiras, trancas, exchangers, etc. O Python não implementa monitores – sinceramente, nunca encontrei uma explicação oficial de porque não – mas possui alguns recursos de sincronização: variáveis de condição, eventos, locks e semáforos. A utilização desses objetos são basicamente iguais em ambas linguagens, para os casos em que existem em ambas linguagens. Python não possui recursos mais complexos como barreiras, trancas ou exchangers. Mas eles podem ser implementados com as classes disponı́veis. Utilizando as primitivas disponı́veis também não é possı́vel garantir ordem como pode ser feito em monitores do Java. Como o leitor já deve ter percebido, as threads de python constituem um módulo ainda inacabado. Foram implementados os recursos básicos para que qualquer outro pudesse existir posteriormente. Um caso é o dos monitores: existem várias implementações na web para solucionar esse problema. Elas fazem uso de anotações, disponı́veis também no Python, e da instrução with para usar em blocos de comandos. Deixando as implementações de terceiros de lado, é possı́vel fazer algo parecido com monitores para blocos de comandos com essa instrução with. Veja o exemplo abaixo: 1 2 3 4 5 6 7 8 9 10 11 12 from t h r e a d i n g import Thread , Lock from time import s l e e p def f o o ( i d ) : with l o c k : for i in xrange ( 5 ) : print i d sleep (0.1) l o c k = Lock ( ) t 1 = Thread ( t a r g e t=foo , a r g s =(1 ,) ) t 2 = Thread ( t a r g e t=foo , a r g s =(2 ,) ) 6 13 t 1 . s t a r t ( ) 14 t 2 . s t a r t ( ) Esse exemplo cria duas threads em que cada uma imprime seu id e dorme por 100ms, repetindo isso por 5 vezes. Se for desconsiderado o uso do Lock, as threads poderiam imprimir em qualquer ordem. Com o uso do Lock, tudo que está dentro do bloco onde ele é utilizado é uma seção crı́tica. Quando essa seção terminar o Lock é liberado. Dessa forma, no exemplo acima, cada thread executa suas operações de uma vez e libera a seção para a outra. A instrução with pode ser usada com locks, semáforos ou variáveis de condição. Qualquer outra implementação de classes de sincronização deve definir um Context Manager (não será explicado neste trabalho) para ser usado nesse tipo de instrução. 3. Desempenho Embora o Python tenha a sintaxe simples de criação de threads, permitir criálas a partir de qualquer objeto que possa ser invocado e possuir os elementos de sincronização básicos e suficientes para realizar qualquer tarefa, o desempenho não é o esperado, como veremos. O Python possui um problema extraordinário que está ligado diretamente ao Global Interpreter Lock ou apenas GIL que será explicado a seguir. Antes de mais nada, podemos constatar o problema na prática e vou demonstrar um exemplo simples de como observá-lo. Veja o código a seguir: 1 2 3 4 5 6 7 8 9 10 11 12 13 from t h r e a d i n g import Thread from time import s l e e p def buzy ( ) : for i in xrange ( 1 6 0 0 0 0 0 0 0 0 ) : pass t 1 = Thread ( t a r g e t=buzy ) t 2 = Thread ( t a r g e t=buzy ) t1 . s t a r t ( ) sleep (30) t2 . s t a r t ( ) Esse programa inicia duas threads, com um intervalo de 30s entre as partidas, que apenas executam uma contagem até um número razoável. Isso serve apenas para ocupar o processador. A partir desse código, foi rodado um experimento em um computador com processador Intel Core 2 Duo 2.4 GHz 2MB Cache L2, 2GB RAM 666MHz e rodando Linux 2.6.28. Como o processador possui dois núcleos, e as threads de Python são implementadas com Pthreads (threads de núcleo no Linux), esperava-se que cada 7 thread ocupasse em torno de 100% de cada núcleo. Entretanto os resultados foram como os exibidos na Figura 1. Figura 1. Resultado de teste de funcionamento de 2 threads em Python em um processador de dois núcleos. Como o processador é de dois núcleos, o valor esperado de uso de CPU para cada thread era de 100%, totalizando os 200% disponı́vel no processador. Porém a soma manteve-se em torno de 100% e cada thread convergiu para algo em torno de 50% de uso. Embora o escalonador do sistema pareça ser conservativo – motivo pelo qual não ouve uma queda brusca quando outra thread entrou em concorrência – o problema pode ser constatado. Isso tudo deve-se ao GIL e como as threads foram implementadas no interpretador da linguagem. 3.1. Global Interpreter Lock Traduzido de [Python.org 2009b]: O interpretador Python não é totalmente thread safe. A fim de suportar programas multithread, existe um lock global, chamado global interpreter lock ou GIL, que precisa ser retido pela thread atual antes que ela possa acessar os demais objetos do Python. Sem o lock, mesmo as operações mais simples poderiam causar problemas em um programa multithread: por exemplo, quando duas threads simultaneamente incrementam o contador de referência do mesmo objeto, o contador poderia acabar sendo incrementado apenas uma vez ao invés de duas. 8 Basicamente, para suportar programas multithread, o interpretador libera e prende o lock a cada 100 instruções de bytecode. O lock também é liberado e preso em torno de operações de IO potencialmente bloqueantes. O GIL foi necessário de acordo como o interpretador já estava implementado na época em que o módulo de threads foi introduzido. É um problema de difı́cil solução sem que o interpretador seja inteiramente reescrito. 3.2. O módulo multiprocessing O leitor deve-se perguntar agora como podemos criar programas que fazem bom uso de múltiplos processadores/núcleos. Simples: criamos novos processos. Um processo novo possui região de memória distinta e trata objetos distintos. O GIL não é problema nesse caso. Porém, perde-se grande desempenho devido as trocas de mensagem entre processos, além do código ficar mais complicado. Como para a questão de desempenho ainda não há solução perfeita e a criação de novos processos é um preço a se pagar, foi criado um módulo que auxilie nessa criação e na troca de dados entre eles. Esse módulo foi chamado de multiprocessing [Python.org 2009c]. Basicamente ele provê métodos de criação, acesso e controle de subprocessos, além de métodos de troca de objetos e o mais importante: sincronização. As mesmas primitivas de sincronização para threads estão disponı́veis para subprocessos. Além de sincronização é possı́vel criar pools de processos – em Java isso é disponı́vel para threads, em Python é inexistente nesse caso – e managers para controlar acesso a memória. 4. O que falta? Definitivamente, livrar-se do GIL. Porém essa é uma tarefa complicada. Já foram propostas soluções mas normalmente elas provocam queda de desempenho em programas de uma única thread. Então, optou-se por não perder nesses casos, já que a grande maioria dos programas no mundo é feita com uma única thread. No caso de não conseguir-se remover o GIL sem perder desempenho para programas single thread, nem ter que reescrever o interpretador novamente, ao menos reduzir o problema seria um bom começo. Para isso já se discute soluções. Além desse problema, é preciso concluir o desenvolvimento do módulo threading, implementando grupos e quem sabe outras primitivas de sincronização e além. 5. Conclusões O Python com certeza mantém o seu ponto, que consiste em criar uma linguagem com alta legibilidade e de sintaxe simples. A criação e manipulação de threads é bastante simples, como foi visto. Embora, na minha opinião, poderiam haver mais alguns elementos simples – e que não consumiriam muito tempo de desenvolvimento da linguagem – como, por exemplo: monitores, barreiras, trancas e etc. Embora já existam implementações disponı́veis na internet, seria interessante colocá-las diretamente na linguagem. Quando comparamos o desempenho, vimos que o Java se sai melhor nesse ponto. O futuro é remover o GIL do Python. Mas uma redução do problema já seria 9 um grande passo. Talvez o GIL desse até mesmo um bom estudo para trabalhos de conclusão em Sistemas Operacionais ou Compiladores. Embora o desempenho seja ruim ainda há o que considerar, como se é possı́vel fazer uso de múltiplos processos. Pois, por exemplo, um programa que não troca muitas mensagens entre eles não terá grande overhead de comunicação. Enfim, eu sempre fui a favor de deixar filosofias e crenças de lado e realmente escolher a linguagem certa para cada problema. No caso de threads ambas linguagens comparadas tem seus pontos fortes. É preciso definir quais deles são essenciais para um projeto na hora de escolher. Referências Peters, T. (2004). Pep 20 – the zen of python. http://www.python.org/dev/peps/ pep-0020. [Online; accessed 22-June-2009]. Python.org (2009a). Classes. http://docs.python.org/tutorial/classes.html. [Online; accessed 20-June-2009]. Python.org (2009b). Initialization, finalization, and threads. http://docs.python. org/c-api/init.html. [Online; accessed 21-June-2009]. Python.org (2009c). multiprocessing – process-based “threading” interface. http:// docs.python.org/library/multiprocessing.html. [Online; accessed 20-June2009]. Python.org (2009d). threading – higher-level threading interface. http://docs. python.org/library/threading.html. [Online; accessed 19-June-2009]. Sun Microsystems (2008). Class thread. http://java.sun.com/javase/6/docs/ api/java/lang/Thread.html. [Online; accessed 19-June-2009]. 10