Back-End

20 mai, 2014

Processamento paralelo com Python

Publicidade

O processamento paralelo, ou assíncrono, permite o uso de threads paralelas para distribuição do processamento. A utilização deste método de processamento possibilita que uma tarefa não tenha que correr de forma sequencial, porém a quebra do processamento em threads deve ocorrer quando as elas não geram dependência entre si, caso contrário elas ficarão bloqueadas até esta dependência ser satisfeita.

O que são threads?

Threads são linhas de execuções que compartilham a área de memória com outras linhas, diferente do processo tradicional, que conta com uma área de memória própria.

As linhas de execução (threads) são criadas dentro de processos. Cada processo conta com ao menos uma linha de execução, sendo possível criar outras linhas dentro do mesmo processo. Quando um processo é finalizado, todas as threads correspondentes a ele são encerradas.

Processamento paralelo

O processamento paralelo utiliza várias linhas de execução para a resolução de um mesmo problema, atacando cada uma delas em uma parte do trabalho e se comunicando para a troca de resultados intermediários ou no mínimo para a divisão inicial do trabalho e para a junção final dos resultados.

ar01

Uma das vantagens de utilizar o processamento paralelo está em explorar com mais eficiência a capacidade da CPU, deste modo é possível aumentar a performance quando se trabalha com processamento de grandes cargas de dados ou quando necessitamos realizar tarefas em background.

Observação: Quando estamos lidando com otimizações e alto desempenho, existe uma quantidade de linhas de execução aceitáveis. Criar o maior número de threads possíveis pode causar um efeito inverso, fazendo o desempenho cair consideravelmente. Então, deve-se tomar cuidado! Realize testes e monitore os processos.

O Python no cenário multithread

O Python dispõe de um módulo nativo chamado thread, que é interpretado por sistemas que suportam o posix threads (pthreads). Também há o módulo threading, que é uma interface de alto nível que provê uma maneira mais fácil de lidar com threads, porém o módulo thread é muito simples de usar.

Para este exemplo vou utilizar a versão 3.3 do CPython.

Para utilizar o módulo _thread, antes de tudo, devemos importá-lo:

import _thread

Vamos agora criar uma tarefa a ser executada:

def task(task_name):
	print(“Thread : %s” % (task_name))

O módulo thread provê uma śerie de funções para manipular as tarefas criadas, porém a mais importante (e que nos interessa) é a função start_new_thread. Ela é a responsável por criar a thread. Para criá-la você deve passar como primeiro parâmetro o nome da função que pretende executar na thread, o segundo parâmetro deverá ser uma tupla com os argumentos que serão passados para a função que será executada.

_thread.start_new_thread(task, (“Tarefa 1”,))
_thread.start_new_thread(task, (“Tarefa 2”,))

Ao executar este código você não verá nada, pois o processo é encerrado rapidamente. Para isso temos que manter o processo aberto, então vamos adicionar o seguinte código:

while True:
            pass

O código deve estar assim:

import _thread

def task(task_name, delay):
	print(“Thread : %s” % (task_name))

_thread.start_new_thread(task, (“Tarefa 1”,))
_thread.start_new_thread(task, (“Tarefa 2”,))

while True:
	pass

Ao executar o código no terminal, não se pode notar se realmente as threads estão sendo criadas. Para que possamos ver as threads em ação vamos criar um delay entre as saídas dos processos. Para ficar mais interessante o nosso código, utilizaremos um loop onde a cada N tempo, uma das threads irá escrever algo na tela.

Para retardar o loop vamos utilizar o módulo time e executar a função sleep.

import _thread
import time

def task(task_name, delay):
	ct = 0
	while ct < 5:
		time.sleep(delay)
		print(“Thread : %s” % (task_name))
		ct += 1

_thread.start_new_thread(task, (“Tarefa 1”, 2))
_thread.start_new_thread(task, (“Tarefa 2”, 4))

while True:
	pass

Agora vamos fazer um incremento ao código. Vamos utilizar uma variável global para definir o número de loops.

import _thread
import time

max_loop = 5

def task(task_name, delay):
	global max_loop
	ct = 0
	while ct < max_loop:
		time.sleep(delay)
		print(“Thread : %s” % (task_name))
		ct += 1

_thread.start_new_thread(task, (“Tarefa 1”, 2))
_thread.start_new_thread(task, (“Tarefa 2”, 4))

while True:
	pass

Vale ressaltar que, diferente dos processos, as threads compartilham a área de memória, então é preciso tomar cuidado ao compartilhar variáveis globais, pois podem haver problemas de concorrência.

É muito simples lidar com processamento paralelo em Python, porém temos um pequeno problema em nosso código: quando que o processo irá morrer?

Manter o processo aberto sem necessidade pode ser péssimo! Por outro lado, se não deixarmos o processo aberto, ele será finalizado quando o código chegar ao fim, e todas as threads abertas serão encerradas. Então, o que fazer? Vamos aproveitar a memória compartilhada e criar um contador para as threads. A ideia é simples: quando a thread for criada, será incrementado 1 ao contador e quando for encerrada vamos decrementar 1. O loop while, que mantém o processo aberto, também será alterado.

import _thread
import time

num_thread = 0
max_loop = 5

def task(task_name, delay):
	global num_thread, max_loop
	num_thread += 1
	ct = 0
	while ct < max_loop:
		time.sleep(delay)
		print(“Thread : %s” % (task_name))
		ct += 1
	num_thread -= 1

_thread.start_new_thread(task, (“Tarefa 1”, 2))
_thread.start_new_thread(task, (“Tarefa 2”, 4))

while num_thread > 0:
	pass

Porém, agora temos outro problema: antes que a num_thread seja incrementada, a nova thread vai para a fila de processamento, então antes que ela seja capaz de incrementar o contador, o processo corrente será finalizado. Para resolver este problema, criaremos uma flag para sinalizar quando a primeira thread começar a ser processada, também teremos que retardar a execução do loop, que contará as threads, criando outro laço que irá aguardar a mudança de estado.

import _thread
import time

num_thread = 0
max_loop = 5
thread_started = False

def task(task_name, delay):
	global num_thread, max_loop, thread_started
	thread_started = True
	num_thread += 1
	ct = 0
	while ct < max_loop:
		time.sleep(delay)
		print(“Thread : %s” % (task_name))
		ct += 1
	num_thread -= 1

_thread.start_new_thread(task, (“Tarefa 1”, 2))
_thread.start_new_thread(task, (“Tarefa 2”, 4))

while not thread_started;
	pass
while num_thread > 0:
	pass

Agora temos o controle do processo. Ao serem encerradas as threads o processo também será finalizado.