Back-End

25 abr, 2012

Threads em Python

Publicidade

Python é uma linguagem bem genérica e fácil de ser utilizada. Qualquer usuário pode aprender a programar em Python de uma maneira bem fácil, principalmente porque a linguagem encapsula conceitos difíceis em implementações fáceis.

Neste artigo vamos tratar da utilização de threads em Python. Threads são fluxos de programas que executam em paralelo dentro de uma aplicação, isto é, uma ramificação de uma parte da aplicação que é executada de forma independente e escalonada independentemente do fluxo inicial da aplicação.

Imaginemos, por exemplo, uma aplicação que mede, de tempos em tempos, a condição de determinados sensores. Supondo que cada sensor precisa ser medido com uma frequência diferente, isto é, um a cada 30 segundos, outro a cada 45 segundos e, por fim, um terceiro a cada 75 segundos.

Implementar isto de maneira sequencial é trabalhoso. Um jeito fácil, porém, é a implementação de uma thread independente para a leitura em cada um dos sensores. Desta forma a thread espera o tempo determinado para a leitura do sensor a que ela está ligada, sem se preocupar, ou mesmo saber, sobre os outros sensores.

Assim, neste caso, bastaria fazer uma classe por tipo de sensor, sendo que cada classe seria uma thread. Para transformar uma classe em thread, são necessárias duas modificações na classe:

  • A classe em questão estender à classe Thread do pacote threading
  • Implementar o método run(), que será chamado quando a thread iniciar

Em Python, o pacote que providencia as funcionalidades de thread é chamado threading, e deve ser importado no começo do seu programa: from threading import Thread.

Segue um exemplo básico, de uma classe chamada Th que implementa Thread e o método run(). O conteúdo do método run será executado em uma thread separada sempre que o método start, definido na classe Thread e herdado pela classe Th no nosso exemplo, for chamado:

       from threading import Thread

class Th(Thread):

def __init__ (self, num):
Thread.__init__(self)
self.num = num

def run(self):

print "Hello "
print self.num


a = Th(1)
a.start()

Apesar de, no exemplo acima, o conteúdo do método run ser executado em uma thread separada, não é possível comprovar isto apenas pela saída do programa.

Afim de comprovarmos que cada thread é executada de forma independente e escalonada independentemente do fluxo inicial da aplicação, vamos analisar o próximo exemplo. Nele criamos várias threads simples Th, como as do exemplo acima, porém ao invés de simplesmente imprimirmos uma mensagem na thread ela vai executar um número definido de vezes COUNTDOWN antes de finalizar sua execução:

            from threading import Thread
import sys

COUNTDOWN = 5

class Th(Thread):

def __init__ (self, num):
sys.stdout.write("Making thread number " + str(num) + "n")
sys.stdout.flush()
Thread.__init__(self)
self.num = num
self.countdown = COUNTDOWN

def run(self):

while (self.countdown):
sys.stdout.write("Thread " + str(self.num) +
" (" + str(self.countdown) + ")n")
sys.stdout.flush()
self.countdown -= 1


for thread_number in range (5):
thread = Th(thread_number)
thread.start()

Uma das possíveis saídas para o programa acima é a seguinte:

               Making thread number 0
Thread 0 (5)
Thread 0 (4)
Thread 0 (3)
Thread 0 (2)
Thread 0 (1)
Making thread number 1
Thread 1 (5)
Making thread number 2
Making thread number 3
Thread 2 (5)
Thread 1 (4)
Thread 1 (3)
Thread 2 (4)
Thread 1 (2)
Thread 2 (3)
Thread 1 (1)
Thread 2 (2)
Making thread number 4
Thread 2 (1)
Thread 3 (5)
Thread 4 (5)
Thread 4 (4)
Thread 3 (4)
Thread 4 (3)
Thread 3 (3)
Thread 4 (2)
Thread 3 (2)
Thread 4 (1)
Thread 3 (1)

Caso você rode o programa acima, a saída não necessariamente será igual a esta, já que a alocação das threads para execução no processador não é um processo determinado. Mesmo rodando múltiplas vezes, o mesmo programa em um mesmo computador as saídas irão variar de uma execução para outra.

Um ponto interessante de se notar no exemplo acima é que, em vez de usarmos print para imprimir na saída padrão, utilizamos sys.stdout.write seguido de uma chamada a sys.stdout.flush.

Isto foi feito para garantir que as mensagems fossem impressas em ordem, já que chamadas a print por diversas threads simultaneamente não garantem a ordem de impressão dos caracteres.

Sincronização de threads

Nos exemplos citados anteriormente, usou-se threads para efetuar processamento paralelos distintos e sem ligação entre si. No entanto, no mundo real muitas vezes as diversas linhas de execução representada pelas threads de um programa precisam, eventualmente, comunicar-se entre si.

Uma forma simples de comunicação é aquela que precisa ocorrer no final do processamento das threads. Exemplos típicos deste tipo de necessidade são programas que processam dados em vetores ou matrizes.

Se estes vetores ou matrizes forem muito grandes e os cálculos efetuados em cada elemento relativamente demorados e independentes até certo ponto, a utilização de threads pode acelerar bastante este tipo de cálculo, já que o trabalho é de alguma forma dividido entre as diversas threads.

Ao final do processo, basta realizarmos um cálculo mais simples que agrega os sub-totais calculados pelas threads. Este tipo de sincronização no final da execução de uma thread pode ser feito através do método join da classe Thread.

Por exemplo, imaginemos um programa que soma os valores de um vetor de inteiros com 1000 elementos cujos valores variam de 0 a 100. Para fins deste exemplo, este vetor será criado com valores randômicos.

Serão criadas quatro threads que calcularão a soma de 250 elementos cada uma. Ao fim do processamento, os sub-totais gerados pelas quatro threads serão somados para gerar um único valor total referente à soma de todos os elementos do vetor.

         from threading import Thread
import random
import sys

NUM_VALUES = 1000
values = []
sequential_total = 0
threaded_total = 0
threads = []
NUM_THREADS = 4

class Th(Thread):
subtotal = 0

def __init__ (self, num):
sys.stdout.write("Making thread number " + str(num) + "n")
sys.stdout.flush()
Thread.__init__(self)
self.num = num

def run(self):

range_start = self.num * NUM_VALUES / NUM_THREADS
range_end = ((self.num + 1) * NUM_VALUES / NUM_THREADS) - 1

for i in range(range_start, range_end):
self.subtotal += values[i]
sys.stdout.write("Subtotal for thread " + str(self.num) +
": " + str(self.subtotal)
+ " (from " + str(range_start)

+ " to " + str(range_end) + ")n");

sys.stdout.flush()

def get_subtotal(self):
return self.subtotal

#### O programa comeca aqui #####

for i in range(NUM_VALUES):
values.append(random.randint(0,100))

for i in range(NUM_VALUES):
sequential_total += values[i]

print("Sequential total: " + str(sequential_total))

for thread_number in range(NUM_THREADS):
threads.insert(thread_number, Th(thread_number))
threads[thread_number].start()

for thread_number in range(NUM_THREADS):
threads[thread_number].join()
threaded_total += threads[thread_number].get_subtotal()

print("Threaded total: " + str(threaded_total))

Um exemplo de saída para o programa acima seria o seguinte:

               Sequential total: 49313
Making thread number 0
Making thread number 1
Subtotal for thread 0: 12365 (from 0 to 249)
Making thread number 2
Subtotal for thread 1: 12568 (from 250 to 499)
Making thread number 3
Subtotal for thread 2: 11742 (from 500 to 749)
Subtotal for thread 3: 12638 (from 750 to 999)
Threaded total: 4931

Note que o valor total calculado pela versão sequencial da soma dos elementos do vetor é igual à soma calculada através das quatro threads criadas no programa para fazer o mesmo trabalho de forma dividida.

Uma outra forma de sincronização pode ser necessária quando há necessidade de se acessar variáveis cujos valores são compartilhados por várias threads rodando simultaneamente. Para isso, é necessário lançar mão da utilização de locks (threading.Lock) ou semáforos (threading.Semaphore). Um artigo a parte descreverá os locks e semáforos, é só aguardar pelo próximo post.

Conclusão

Este artigo fez uma introdução a programação com Thread em python, mostrando o que é uma thread, com deve ser construida classes que tornam-se thread e como sincronizar o fim das threads.

Recursos

***

artigo publicado originalmente no developerWorks Brasil, por Breno Leitão

Breno Leitão é engenheiro de software pela IBM onde atua como kernel hacker.