Programação multi-thread em python

Esse talvez possa parecer um post meio maluco, para o que comumente discutimos aqui nesse blog, mas é muito interessante e curioso, para saber o que está acontecendo sobre o capo do carro, ou do computador.

Uma das coisas mais diferentes que já vi, foi quando fiz a disciplina de sistemas operacionais, isso porque tudo que eu achava que sabia sobre computador, vi que estava errado. Nem tudo é tão simples como um script continuo que fazemos aqui no R, e uma prova disso é a programação Multi-thread.

thread

Vamos pensar na figura acima, tomando como exemplo os MCMC que vemos comumente aqui, veja alguma exemplos de implementação aqui de um gibbs samples e aqui do mais simples metropolis hastings.

Nesses dois casos fizemos algo como em single-threaded, isso porque fizemos uma cadeia e fomos realizando vários cálculos e salvando resultados. Acontece que, todas as analises de dados que fazemos, não usamos apenas uma cadeia, usamos várias cadeias, no último post mesmo, usando o openbugs, usamos três cadeias, que podem ser processadas separadamente, podem ser multithreaded.
Os valores das amostras, parâmetros, modelos, tudo é igual, então não compensa gravar isso varias vezes na memoria, é melhor compartilhar, mas para uma cadeia de markov, a gente sempre precisa do valor anterior para continuar ela, então as cadeias não precisam dividir os cálculos que estão fazendo entre si, apenas os dados e parâmetros e modelos, agora olha no modelinho ali em cima, não é exatamente isso que diz respeito a programação multi-thread?
Veja que com multi-thread, cada thread tem sua pilha, seus registradores, seus dados particulares, que é sua cadeia de markov que está computando, mas também divide dados, que são as amostras, modelo, parâmetros entre todas as threads.

E qual a relevância disso? Ai que entre aqueles processadores de muitos núcleos, sem isso, sem paralelizar, dividir a computação, a gente teria que fazer uma cadeia no processador, terminar, fazer outra, terminar, pense que leva um minuto por cadeia, com três então levamos três minutos para fazer tudo, mas se fizermos algo multi-thread, da para mandar cada cadeia para cada núcleo do processador (mais ou menos isso, simplificando, sei que não é assim), como cada um leva 1 minuto e da para fazer em paralelo, ao invés de esperar 3 minutos pelo resultado, esperamos um.

image0021225707175745

Podemos fazer um exemplo em python que é bem simples, usando a biblioteca threading.

Para esse exemplo simples, vamos criar uma classe bem bobinha

1
2
3
4
5
6
7
8
9
10
class minhaThread (threading.Thread):
    def __init__(self, threadID, nome, contador):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.nome = nome
        self.contador = contador
    def run(self):
        print "Iniciando thread %s com %d processos" % (self.name,self.contador)
        processo(self.nome, self.contador)
        print "Finalizando " + self.nome

A gente ja falou de orientação a objetos em R aqui e aqui, uma das coisas que a orientação a objetos permite é a herança, que é herdar tudo que a classe pai tem, aqui estamos herdando a classe Thread definida na biblioteca threading, isso é o que esta escrito aqui:

1
class minhaThread (threading.Thread):

Por isso o parenteses, lembro que ja usei classe em algum problema do rosalind aqui, mas não lembro em qual post agora, mas beleza, o resto é bem besta a classe vai ter 3 atributos, um ID, um nome e um contador, que seria algo como os parâmetros para fazer um mcmc, vamos abstrair de uma atividade útil agora, vamos apenas pensar em termos de computação.

1
2
3
4
5
    def __init__(self, threadID, nome, contador):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.nome = nome
        self.contador = contador

Então nos construtor da thread definimos esses atributos e ela vai ter apenas um método, uma função chamada run.

1
2
3
4
    def run(self):
        print "Iniciando thread %s com %d processos" % (self.name,self.contador)
        processo(self.nome, self.contador)
        print "Finalizando " + self.nome

Tem que chamar run, por causa que é assim que funciona o pacote, mas o run vai so imprimir que estamos começando, processar algo, pensando que processar pode ser qualquer coisa e depois avisar que terminamos.

Aqui o processo definimos assim

1
2
3
4
def processo(nome, contador):
    while contador:
        print "Thread %s fazendo o processo %d" % (nome, contador)
        contador -= 1

Ele é uma função externa, não é da classe, mas ele apenas imprimi uma mensagem, falando que esta processando, e diminui o contador em 1, até zerar, e terminar.

Depois disso está tudo pronto, so precisamos criar nossas threads

1
2
3
# Criando as threads
thread1 = minhaThread(1, "Alice", 8)
thread2 = minhaThread(2, "Bob", 8)

Criamos elas, simples assim, aqui no caso são duas, chamadas de alice e bob, hehe, alice e bob são exemplos mais comuns em física, criptografia e teoria dos jogos, ao invés de falar A e B, os caras falam Alice e Bob, para ficar mais fácil de entender.

Ok, mas apos criado, nada foi processado ainda, dai então começamos elas.

1
2
3
# Comecando novas Threads
thread1.start()
thread2.start()

O resto do programa é meramente para fazer a thread mãe esperar alice e bob para terminar, não convêm explicar agora, mas essa parte so faz isso, esperar alice e bob terminarem

1
2
3
4
5
6
threads = []
threads.append(thread1)
threads.append(thread2)
 
for t in threads:
    t.join()

Agora é legal quando a gente roda esse programa.

As vezes ele roda uma thread seguida da outra

Screenshot from 2015-07-23 16:21:42

E as vezes tudo vira uma loucura.

Screenshot from 2015-07-23 16:23:00

Isso porque uma vez que você inicio o processamento, o sistema operacional que vai escalonar as threads, dai a gente não manda mais, só o sistema operacional, então se o processador ta liberado, ele manda elas para la para fazer conta, mas as vezes ta ocupado, então a thread tem que esperar sua vez, agora uma thread pode ir para um núcleo e ir até o final la, enquanto a outra entrou num outro e teve que esperar alguém terminar de processar, como no segundo exemplo, Alice começou, o Bob esperou um pouco, Alice ja tinha processado 2 vezes, ai o Bob começou a trabalhar, mas terminou depois.

Um exemplo bem besta, mas não é preciso saber fazer muitas contas complexas para imaginar que isso é melhor que primeiro alice fazer todo o trabalho dela e depois bob fazer todo o trabalho dela. Pena que isso é extremamente complexo, ainda mais quando a gente tem que compartilhar algum dado e mexer nele, porque ai a vez de quem mexeu pode ser importante, mas isso é outra historia.

Claro que programação não é a meta de muitas pessoas que visitam aqui, mas ter uma ideia que isso existe, pode ajudar a entender comentários de manuais de programa entre outras coisas.

Por exemplo, um programa comum em biologia é o mrBayes, basta olhar o manual que você vai notar alguma notas de instalação, veja essa parte aqui:

instructions for compiling and running the parallel version of the program

Ou seja, se você instala as coisas so dando dois cliques, não le o manual, pode estar perdendo essa possibilidade de usar multi-thread e algo que pode rodar em uma hora vai levar um dia. Ja que ainda no exemplo do mrBayes, da para usar até o processador da placa de video para ajudar a fazer cálculos e agilizar o processamento. E pode abrir o olho, que tudo que você ler MCMC é um forte candidato a usar multi-thread.

O R em si não é multi-thread, mas existem vários pacotes que tentam incorporar isso, como visto no taskview de high performance. Além de que o R possui interfaces que ligam eles a muitos outros programas, como é o caso do Openbugs mesmos, que os calculos são feitos neles, mas depois devolvemos os dados no R para fazer figuras e outras analises, mas temos muitos outros exemplo.

Apesar que na maioria das vezes isso não é muito relevante, para fazer regressões e muitos modelos, tudo é tão rápido que a gente nem sente diferença, mas algumas coisas mais avançadas (talvez “avançando” seja um termo ruim aqui, coisas que precisam fazer mais contas, comum em evolução), isso começa a ser importante.

Bem é isso ai, a primeira vez que ouvi sobre esse negocio de multi-thread, a primeira coisa que veio na minha cara é MCMC, acho que ter ouvido falar de MCMC antes de fazer Sistemas Operacionais foi um diferencial para tornar tudo mais interessante e belo, mas tive um excelente professor em sistemas operacionais também para ajudar. O script vai estar la no repositório recologia, tem mais alguma exemplos da minha aula de sistemas operacionais no github em outro repositório aqui mas em linguagem C, e se eu escrevi alguma bobeira, algo errado, deixe um comentário corrigindo ou mande um e-mail e até mais.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import threading
 
class minhaThread (threading.Thread):
    def __init__(self, threadID, nome, contador):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.nome = nome
        self.contador = contador
    def run(self):
        print "Iniciando thread %s com %d processos" % (self.name,self.contador)
        processo(self.nome, self.contador)
        print "Finalizando " + self.nome
 
def processo(nome, contador):
    while contador:
        print "Thread %s fazendo o processo %d" % (nome, contador)
        contador -= 1
 
# Criando as threads
thread1 = minhaThread(1, "Alice", 8)
thread2 = minhaThread(2, "Bob", 8)
 
# Comecando novas Threads
thread1.start()
thread2.start()
 
threads = []
threads.append(thread1)
threads.append(thread2)
 
for t in threads:
    t.join()
 
print "Saindo da main"

4 thoughts on “Programação multi-thread em python

Leave a Reply

Your email address will not be published. Required fields are marked *