Este artigo aborda o suporte a threads em Python, incluindo os fundamentos necessários para programação multithread e uma apresentação completa com exemplos de uso das duas bibliotecas padrão de threads do Python.
Nota: Este artigo foi baseado na versão 2.4 do Python. Para termos desconhecidos, consulte recursos como Baidu, Google ou Wikipedia.
- Conceitos Básicos de Threads
1.1. Estados de Threads
As threads possuem 5 estados distintos, e as transições entre esses estados são ilustradas na figura abaixo:
1.2. Sincronização de Threads (Locks)
A vantagem da programação multithread reside na capacidade de executar múltiplas tarefas simultaneamente (pelo menos aparentemente). No entanto, quando threads precisam compartilhar dados, podem surgir problemas de sincronização. Considere o seguinte cenário: uma lista contém apenas elementos 0. Uma thread "modificadora" percorre a lista do fim ao início, alterando todos os elementos para 1, enquanto uma thread "impressora" lê e imprime a lista do início ao fim. É possível que a thread "impressora" comece a imprimir enquanto a thread "modificadora" ainda está alterando os elementos, resultando em uma saída parcialmente com 0s e parcialmente com 1s. Essa é uma situação de dessincronização.
Para evitar esse problema, introduzimos o conceito de lock (bloqueio).
Os locks possuem dois estados: bloqueado e desbloqueado. Sempre que uma thread, como a "modificadora", precisa acessar dados compartilhados, ela primeiro deve adquirir o lock. Se outra thread, como a "impressora", já possui o lock, a thread "modificadora" é suspensa em um bloqueio síncrono. Apenas quando a thread "impressora" terminar seu acesso e liberar o lock, a thread "modificadora" pode prosseguir. Com essa abordagem, a impressão da lista resultará em todos 0s ou todos 1s, evitando a situação ambígua de valores mistos.
A interação entre threads e locks é ilustrada na figura abaixo:
1.3. Comunicação entre Threads (Variáveis de Condição)
Existe outro cenário problemático: a lista não existe inicialmente; ela é criada por uma thread "criadora". Se as threads "modificadora" ou "impressora" tentarem acessar a lista antes da thread "criadora" executá-las, uma exceção será lançada. Embora os locks possam resolver esse problema, as threads "modificadora" e "impressora" precisariam de um loop infinito - elas não saberiam quando a thread "criadora" seria executada. Uma solução melhor seria permitir que a thread "criadora" notifique as outras threads após sua execução. É aqui que as variáveis de condição entram em cena.
As variáveis de condição permitem que threads, como "modificadora" e "impressora", esperem quando a condição não é atendida (a lista é None). Quando a condição é satisfeita (a lista foi criada), uma notificação é enviada, informando às threads que a condição foi atendida e que elas podem prosseguir com sua execução.
A interação entre threads e variáveis de condição é ilustrada na figura abaixo:
1.4. Transições entre estados de execução e bloqueio
Finalmente, vamos examinar as transições entre os estados de execução e bloqueio das threads.
O bloqueio pode ocorrer em três situações:
- Bloqueio síncrono: ocorre quando uma thread está competindo por um lock. Quando uma thread solicita um lock, ela entra neste estado. Após obter com sucesso o lock, ela retorna ao estado de execução.
- Bloqueio de espera: ocorre quando uma thread está esperando por notificações de outras threads. Depois de obter um lock de condição, uma thread chama "wait" e entra neste estado. Quando outra thread envia uma notificação, a thread entra no estado de bloqueio síncrono, competindo novamente pelo lock de condição.
- Outros bloqueios: ocorrem ao chamar time.sleep(), anotherthread.join() ou ao esperar por operações de E/S. Nesse estado, a thread não libera os locks já obtidos.
Dica: Se você compreender esses conceitos, os tópicos seguintes serão bastante fáceis de assimilar. Além disso, esses conceitos são semelhantes na maioria das linguagens de programação populares.
- Módulo thread
Python oferece suporte a threads através de dois módulos padrão: thread e threading. O módulo thread fornece threads de baixo nível, primitivas e um simples mecanismo de lock.
1 # codificação: UTF-8
2 import thread
3 import time
4
5 # Função a ser executada em uma thread
6 def tarefa():
7 for i in range(5):
8 print 'tarefa'
9 time.sleep(1)
10
11 # Finaliza a thread atual
12 # Este método é equivalente a thread.exit_thread()
13 thread.exit() # Quando tarefa retorna, a thread também termina
14
15 # Inicia uma thread, que começa a executar imediatamente
16 # Este método é equivalente a thread.start_new_thread()
17 # O primeiro parâmetro é a função, o segundo são os argumentos da função
18 thread.start_new(tarefa, ()) # Quando a função não tem parâmetros, é necessário passar uma tupla vazia
19
20 # Cria um lock (LockType, não pode ser instanciado diretamente)
21 # Este método é equivalente a thread.allocate_lock()
22 bloqueio = thread.allocate()
23
24 # Verifica se o lock está no estado bloqueado ou liberado
25 print bloqueio.locked()
26
27 # O lock geralmente é usado para controlar o acesso a recursos compartilhados
28 contador = 0
29
30 # Adquire o lock, retorna True se obtiver com sucesso o bloqueio
31 # O parâmetro opcional timeout, quando não especificado, bloqueia até obter o lock
32 # Caso contrário, retorna False após o tempo limite
33 if bloqueio.acquire():
34 contador += 1
35
36 # Libera o lock
37 bloqueio.release()
38
39 # As threads fornecidas pelo módulo thread terminarão simultaneamente após o término da thread principal
40 time.sleep(6)
Outros métodos fornecidos pelo módulo thread: thread.interrupt_main(): Termina a thread principal a partir de outra thread. thread.get_ident(): Retorna um número mágico que representa a thread atual, frequentemente usado para obter dados relacionados a threads de um dicionário. Esse número em si não tem significado e será reutilizado por novas threads após o término da thread original.
O módulo thread também fornece uma classe ThreadLocal para gerenciar dados relacionados a threads, chamada thread._local, que é referenciada no módulo threading.
Devido à limitação do módulo thread - como a incapacidade de continuar executando após o término da thread principal, a falta de suporte a variáveis de condição e outras funcionalidades - geralmente não é utilizado em aplicações reais, por isso não será abordado detalhadamente neste artigo.
- threading
O módulo threading foi projetado com base no modelo de threads do Java. Enquanto em Java os locks e variáveis de condição são comportamentos fundamentais de objetos (cada objeto possui seu próprio lock e variável de condição), em Python eles são objetos independentes. A classe Thread do Python oferece um subconjunto das funcionalidades da Thread do Java; não há suporte a prioridades, grupos de threads, e threads não podem ser paradas, pausadas, retomadas ou interrompidas. Alguns métodos estáticos implementados na Thread do Java são disponibilizados no módulo threading como métodos de módulo.
**Métodos comuns fornecidos pelo módulo threading:**threading.currentThread(): Retorna a variável da thread atual. threading.enumerate(): Retorna uma lista contendo as threads atualmente em execução. Uma thread está em execução após sua inicialização e antes de seu término, excluindo threads antes da inicialização e após o término. threading.activeCount(): Retorna o número de threads em execução, tendo o mesmo resultado que len(threading.enumerate()).
Classes fornecidas pelo módulo threading: Thread, Lock, Rlock, Condition, [Bounded]Semaphore, Event, Timer, local.
3.1. Thread
A classe Thread representa uma thread, similar à implementação em Java. Existem duas formas de utilização: passando diretamente o método a ser executado ou herdando de Thread e sobrescrevendo o método run():
1 # codificação: UTF-8
2 import threading
3
4 # Método 1: Passar o método a ser executado como parâmetro para o construtor de Thread
5 def processar():
6 print 'processar() passado para Thread'
7
8 t = threading.Thread(target=processar)
9 t.start()
10
11 # Método 2: Herdar de Thread e sobrescrever run()
12 class MinhaThread(threading.Thread):
13 def run(self):
14 print 'MinhaThread herdada de Thread'
15
16 t = MinhaThread()
17 t.start()
Método construtor: Thread(group=None, target=None, name=None, args=(), kwargs={}) group: Grupo de threads, atualmente não implementado, deve ser None; target: Método a ser executado; name: Nome da thread; args/kwargs: Parâmetros a serem passados para o método.
Métodos de instância: isAlive(): Retorna se a thread está em execução. Uma thread está em execução após sua inicialização e antes de seu término. get/setName(name): Obter/definir o nome da thread. is/setDaemon(bool): Obter/definir se é uma thread do sistema. O valor inicial é herdado da thread que a criou. Quando não há mais threads do sistema em execução, o programa será encerrado. start(): Inicia a thread. join([timeout]): Bloqueia o thread do contexto atual até que o thread que chamou este método seja encerrado ou atinja o timeout (parâmetro opcional).
Exemplo de uso de join():
# codificação: UTF-8
import threading
import time
def contexto(tJuncao):
print 'no threadContext.'
tJuncao.start()
# Bloqueará tContext até threadJoin ser encerrada.
tJuncao.join()
# Após o término de tJuncao, a execução continua.
print 'fora de threadContext.'
def juntar():
print 'no threadJoin.'
time.sleep(1)
print 'fora de threadJoin.'
tJuncao = threading.Thread(target=juntar)
tContexto = threading.Thread(target=contexto, args=(tJuncao,))
tContexto.start()
Resultado da execução:
no threadContext.
no threadJoin.
fora de threadJoin.
fora de threadContext.
3.2. Lock
Lock (bloqueio de instrução) é o nível mais baixo de instrução de sincronização disponível. Quando está no estado bloqueado, um Lock não é possuído por uma thread específica. Lock contém dois estados - bloqueado e desbloqueado - e dois métodos básicos.
Pode-se considerar que um Lock possui um pool de bloqueio. Quando uma thread solicita um bloqueio, ela é colocada no pool e só sai quando obtém o bloqueio. As threads no pool estão no estado de bloqueio síncrono do diagrama de estados.
Método construtor: Lock()
Métodos de instância: acquire([timeout]): Coloca a thread em estado de bloqueio síncrono, tentendo obter o bloqueio. release(): Libera o bloqueio. Deve ser usado apenas após a thread ter obtido o bloqueio, caso contrário, uma exceção será lançada.
# codificação: UTF-8
import threading
import time
dados = 0
bloqueio = threading.Lock()
def funcao():
global dados
print '%s adquirindo bloqueio...' % threading.currentThread().getName()
# Ao chamar acquire([timeout]), a thread ficará bloqueada
# até obter o bloqueio ou até que timeout segundos se passem (timeout é opcional).
# Retorna se obteve o bloqueio.
if bloqueio.acquire():
print '%s obteve o bloqueio.' % threading.currentThread().getName()
dados += 1
time.sleep(2)
print '%s liberando bloqueio...' % threading.currentThread().getName()
# Chamar release() liberará o bloqueio.
bloqueio.release()
t1 = threading.Thread(target=funcao)
t2 = threading.Thread(target=funcao)
t3 = threading.Thread(target=funcao)
t1.start()
t2.start()
t3.start()
3.3. RLock
RLock (bloqueio reentrante) é uma instrução de sincronização que pode ser solicitada várias vezes pela mesma thread. RLock usa os conceitos de "thread proprietária" e "nível de recursão". Quando está no estado bloqueado, um RLock é possuído por uma thread. A thread que possui o RLock pode chamar acquire() novamente, e para liberar o bloqueio, release() deve ser chamado o mesmo número de vezes.
Pode-se considerar que um RLock contém um pool de bloqueio e um contador inicializado em 0. Cada chamada bem-sucedida de acquire()/release() incrementa/decrementa o contador em 1. Quando o contador atinge 0, o bloqueio está no estado desbloqueado.
Método construtor: RLock()
Métodos de instância: acquire([timeout])/release(): Semelhante ao Lock.
# codificação: UTF-8
import threading
import time
rlock = threading.RLock()
def funcao():
# Primeira solicitação de bloqueio
print '%s adquirindo bloqueio...' % threading.currentThread().getName()
if rlock.acquire():
print '%s obteve o bloqueio.' % threading.currentThread().getName()
time.sleep(2)
# Segunda solicitação de bloqueio
print '%s adquirindo bloqueio novamente...' % threading.currentThread().getName()
if rlock.acquire():
print '%s obteve o bloqueio.' % threading.currentThread().getName()
time.sleep(2)
# Primeira liberação do bloqueio
print '%s liberando bloqueio...' % threading.currentThread().getName()
rlock.release()
time.sleep(2)
# Segunda liberação do bloqueio
print '%s liberando bloqueio...' % threading.currentThread().getName()
rlock.release()
t1 = threading.Thread(target=funcao)
t2 = threading.Thread(target=funcao)
t3 = threading.Thread(target=funcao)
t1.start()
t2.start()
t3.start()
3.4. Condition
Condition (variável de condição) geralmente está associada a um bloqueio. Quando é necessário compartilhar um bloqueio entre múltiplas Conditions, uma instância de Lock/RLock pode ser passada para o método construtor; caso contrário, uma instância de RLock será criada automaticamente.
Pode-se dizer que, além do pool de bloqueio do Lock, uma Condition também contém um pool de espera. As threads no pool de espera estão no estado de bloqueio de espera do diagrama de estados, até que outra thread chame notify()/notifyAll() para notificá-las. Após receber a notificação, as threads entram no pool de bloqueio esperando para obter o bloqueio.
Método construtor: Condition([lock/rlock])
Métodos de instância: acquire([timeout])/release(): Chama os métodos correspondentes do bloqueio associado. wait([timeout]): Chamar este método coloca a thread no pool de espera da Condition, aguardando notificação e liberando o bloqueio. A thread deve já ter obtido o bloqueio, caso contrário, uma exceção será lançada. notify(): Chama este método para selecionar uma thread do pool de espera e notificá-la. A thread notificada chamará automaticamente acquire() para tentar obter o bloqueio (entrando no pool de bloqueio). As outras threads permanecem no pool de espera. Chamar este método não libera o bloqueio. A thread deve já ter obtido o bloqueio, caso contrário, uma exceção será lançada. notifyAll(): Chama este método para notificar todas as threads no pool de espera. Essas threads entrarão no pool de bloqueio tentando obter o bloqueio. Chamar este método não libera o bloqueio. A thread deve já ter obtido o bloqueio, caso contrário, uma exceção será lançada.
Um exemplo comum é o padrão produtor/consumidor:
# codificação: UTF-8
import threading
import time
# Produto
produto = None
# Variável de condição
cond = threading.Condition()
# Método do produtor
def produzir():
global produto
if cond.acquire():
while True:
if produto is None:
print 'produzindo...'
produto = 'qualquer_coisa'
# Notifica o consumidor que o produto foi produzido
cond.notify()
# Espera notificação
cond.wait()
time.sleep(2)
# Método do consumidor
def consumir():
global produto
if cond.acquire():
while True:
if produto is not None:
print 'consumindo...'
produto = None
# Notifica o produtor que o produto foi consumido
cond.notify()
# Espera notificação
cond.wait()
time.sleep(2)
t1 = threading.Thread(target=produzir)
t2 = threading.Thread(target=consumir)
t2.start()
t1.start()
3.5. Semaphore/BoundedSemaphore
Semaphore (semáforo) é uma das instruções de sincronização mais antigas da história da ciência da computação. Semaphore gerencia um contador interno. Sempre que acquire() é chamado, o contador é decrementado em 1, e quando release() é chamado, é incrementado em 1. O contador não pode ser menor que 0; quando o contador é 0, acquire() bloqueará a thread no estado de bloqueio síncrono até que outra thread chame release().
Com base nessa característica, Semaphore é frequentemente usado para sincronizar objetos com "limite de visitantes", como pools de conexões.
A única diferença entre BoundedSemaphore e Semaphore é que o primeiro verifica ao chamar release() se o valor do contador excede o valor inicial. Se exceder, uma exceção será lançada.
**Método construtor:**Semaphore(value=1): value é o valor inicial do contador.
Métodos de instância: acquire([timeout]): Solicita o Semaphore. Se o contador for 0, a thread será bloqueada no estado de bloqueio síncrono; caso contrário, o contador será decrementado em 1 e o método retornará imediatamente. release(): Libera o Semaphore, incrementando o contador em 1. Se estiver usando BoundedSemaphore, também verificará se o número de liberações excede o valor inicial. O método release() não verifica se a thread já obteve o Semaphore.
# codificação: UTF-8
import threading
import time
# Valor inicial do contador é 2
semaforo = threading.Semaphore(2)
def funcao():
# Solicita o Semaphore, após sucesso o contador é decrementado;
# quando o contador é 0, a thread é bloqueada
print '%s solicitando semáforo...' % threading.currentThread().getName()
if semaforo.acquire():
print '%s obteve semáforo' % threading.currentThread().getName()
time.sleep(4)
# Libera o Semaphore, o contador é incrementado
print '%s liberando semáforo' % threading.currentThread().getName()
semaforo.release()
t1 = threading.Thread(target=funcao)
t2 = threading.Thread(target=funcao)
t3 = threading.Thread(target=funcao)
t4 = threading.Thread(target=funcao)
t1.start()
t2.start()
t3.start()
t4.start()
time.sleep(2)
# A thread principal que não obteve o semáforo também pode chamar release
# Se usar BoundedSemaphore, t4 ao liberar o semáforo lançará uma exceção
print 'Thread principal liberando semáforo sem adquirir'
semaforo.release()
3.6. Event
Event (evento) é um dos mecanismos mais simples de comunicação entre threads: uma thread notifica um evento e outras threads esperam por esse evento. Event possui internamente um sinal inicializado como False. Quando set() é chamado, o sinal é definido como True, e quando clear() é chamado, ele é redefinido como False. wait() bloqueará a thread no estado de espera até que o sinal seja definido como True.
Event na verdade é uma versão simplificada de Condition. Event não possui bloqueio e não pode colocar threads no estado de bloqueio síncrono.
Método construtor: Event()
Métodos de instância: isSet(): Retorna True quando o sinal interno estiver True. set(): Define o sinal como True e notifica todas as threads no estado de bloqueio de espera para retornarem ao estado de execução. clear(): Define o sinal como False. wait([timeout]): Se o sinal for True, retornará imediatamente; caso contrário, bloqueará a thread no estado de espera até que outra thread chame set().
# codificação: UTF-8
import threading
import time
evento = threading.Event()
def funcao():
# Espera o evento, entra em estado de bloqueio de espera
print '%s esperando evento...' % threading.currentThread().getName()
evento.wait()
# Recebe o evento e entra em estado de execução
print '%s recebeu evento.' % threading.currentThread().getName()
t1 = threading.Thread(target=funcao)
t2 = threading.Thread(target=funcao)
t1.start()
t2.start()
time.sleep(2)
# Envia notificação do evento
print 'Thread principal definindo evento.'
evento.set()
3.7. Timer
Timer (temporizador) é uma classe derivada de Thread, usada para chamar um método após um tempo especificado.
Método construtor: Timer(interval, function, args=[], kwargs={}) interval: Tempo especificado function: Método a ser executado args/kwargs: Parâmetros do método
Métodos de instância: Timer é derivada de Thread e não adiciona métodos de instância.
1 # codificação: UTF-8
2 import threading
3
4 def funcao():
5 print 'olá temporizador!'
6
7 temporizador = threading.Timer(5, funcao)
8 temporizador.start()
3.8. local
local é uma classe que começa com letra minúscula, usada para gerenciar dados thread-local (dados específicos de cada thread). Para o mesmo objeto local, uma thread não pode acessar atributos definidos por outras threads; atributos definidos por uma thread não substituem atributos com o mesmo nome definidos por outras threads.
Pode-se considerar local como um "dicionário de threads-atributos", onde local encapsula os detalhes de检索ar um dicionário de atributos usando a thread como chave e depois检索ar o valor do atributo usando o nome do atributo como chave.
1 # codificação: UTF-8
2 import threading
3
4 local = threading.local()
5 local.tnome = 'principal'
6
7 def funcao():
8 local.tnome = 'não_principal'
9 print local.tnome
10
11 t1 = threading.Thread(target=funcao)
12 t1.start()
13 t1.join()
14
15 print local.tnome
Com domínio das classes Thread, Lock e Condition, você pode lidar com a maioria das situações que exigem o uso de threads. Em certos casos, local também é bastante útil. Finalmente, usaremos essas classes para ilustrar os cenários mencionados nos conceitos básicos de threads:
1 # codificação: UTF-8
2 import threading
3
4 lista = None
5 condicao = threading.Condition()
6
7 def definir():
8 if condicao.acquire():
9 while lista is None:
10 condicao.wait()
11 for i in range(len(lista))[::-1]:
12 lista[i] = 1
13 condicao.release()
14
15 def imprimir():
16 if condicao.acquire():
17 while lista is None:
18 condicao.wait()
19 for i in lista:
20 print i,
21 print
22 condicao.release()
23
24 def criar():
25 global lista
26 if condicao.acquire():
27 if lista is None:
28 lista = [0 for i in range(10)]
29 condicao.notifyAll()
30 condicao.release()
31
32 tdefinir = threading.Thread(target=definir,name='tdefinir')
33 timprimir = threading.Thread(target=imprimir,name='timprimir')
34 tcriar = threading.Thread(target=criar,name='tcriar')
35 tdefinir.start()
36 timprimir.start()
37 tcriar.start()