Os sistemas operacionais modernos suportam multitarefa, permitindo a execução simultânea de programas ou a divisão de tarefas em subunidades independentes. Em ambientes com múltiplos CPUs ou núcleos, a programação concorrente é essencial para otimizar o desempenho e a experiência do usuário. Conceitos fundamentais incluem processos e threads.
Conceitos Básicos
Um processo é uma instância em execução de um programa, com seu próprio espaço de endereçamento, pilha de dados e estruturas auxiliares. O sistema operacional aloca recursos e gerencia a execução dos processos. Novos processos podem ser criados via chamadas como fork em sistemas Unix, mas precisam de mecanismos de comunicação entre processos (IPC) — como pipes, sockets ou memória compartilhada — para trocar dados.
Threads são unidades de execução dentro de um processo, compartilhando o mesmo contexto de memória. Isso facilita a comunicação entre elas, mas em sistemas de núcleo único, a concorrência é simulada por escalonamento. A programação multithread melhora o desempenho, mas pode consumir recursos excessivos e dificultar a depuração.
O Python oferece suporte tanto a multiprocessamento quanto a multithreading, possibilitando três abordagens concorrentes: múltiplos processos, múltiplas threads ou uma combinação de ambos.
Multiprocessamenot em Python
Em sistemas Unix, a função fork() do módulo os cria processos filhos. Para portabilidade cross-platform, utiliza-se a classe Process do módulo multiprocessing, que inclui ferramentas como pools de processos e filas para comunicação.
Considere uma tarefa de download sequencial versus concorrente. O código abaixo simula downloads sem concorrência:
import random
import time
def baixar_arquivo(nome_arquivo):
print(f'Iniciando download de {nome_arquivo}...')
tempo_download = random.randint(5, 10)
time.sleep(tempo_download)
print(f'{nome_arquivo} concluído! Tempo: {tempo_download} segundos')
def principal():
inicio = time.time()
baixar_arquivo('documento.pdf')
baixar_arquivo('video.mp4')
fim = time.time()
print(f'Tempo total: {fim - inicio:.2f} segundos')
if __name__ == '__main__':
principal()
Este código executa os downloads em série, resultando em tempo acumulado. Para paralelização, usamos Process:
import multiprocessing
import random
import time
def baixar_arquivo(nome_arquivo):
pid = multiprocessing.current_process().pid
print(f'Processo {pid} iniciado para {nome_arquivo}.')
tempo_download = random.randint(5, 10)
time.sleep(tempo_download)
print(f'{nome_arquivo} concluído! Tempo: {tempo_download} segundos')
def principal():
inicio = time.time()
p1 = multiprocessing.Process(target=baixar_arquivo, args=('documento.pdf',))
p2 = multiprocessing.Process(target=baixar_arquivo, args=('video.mp4',))
p1.start()
p2.start()
p1.join()
p2.join()
fim = time.time()
print(f'Tempo total: {fim - inicio:.2f} segundos')
if __name__ == '__main__':
principal()
A concorrência reduz o tempo total, mas cada processo mantém seu próprio estado. Para comunicação entre processos, utilize multiprocessing.Queue, evitando variáveis globais isoladas.
Multithreading em Python
O módulo threading fornece uma interface orientada a objetos para threads. As threads compartilham memória, facilitando a comunicação, mas exigindo sincronização para recursos críticos.
Reescrevendo o exemplo de download com threads:
import random
import threading
import time
def baixar_arquivo(nome_arquivo):
print(f'Iniciando download de {nome_arquivo}...')
tempo_download = random.randint(5, 10)
time.sleep(tempo_download)
print(f'{nome_arquivo} concluído! Tempo: {tempo_download} segundos')
def principal():
inicio = time.time()
t1 = threading.Thread(target=baixar_arquivo, args=('documento.pdf',))
t2 = threading.Thread(target=baixar_arquivo, args=('video.mp4',))
t1.start()
t2.start()
t1.join()
t2.join()
fim = time.time()
print(f'Tempo total: {fim - inicio:.3f} segundos')
if __name__ == '__main__':
principal()
Threads também podem ser criadas por herança:
import random
import threading
import time
class TarefaDownload(threading.Thread):
def __init__(self, nome_arquivo):
super().__init__()
self.nome_arquivo = nome_arquivo
def run(self):
print(f'Iniciando download de {self.nome_arquivo}...')
tempo_download = random.randint(5, 10)
time.sleep(tempo_download)
print(f'{self.nome_arquivo} concluído! Tempo: {tempo_download} segundos')
def principal():
inicio = time.time()
t1 = TarefaDownload('documento.pdf')
t2 = TarefaDownload('video.mp4')
t1.start()
t2.start()
t1.join()
t2.join()
fim = time.time()
print(f'Tempo total: {fim - inicio:.2f} segundos')
if __name__ == '__main__':
principal()
O compartilhamento de recursos entre threads pode levar a condições de corrida. Exemplo com uma conta bancária sem proteção:
import threading
import time
class ContaBancaria:
def __init__(self):
self.saldo = 0
def depositar(self, valor):
novo_saldo = self.saldo + valor
time.sleep(0.01) # Simula processamento
self.saldo = novo_saldo
class TransacaoThread(threading.Thread):
def __init__(self, conta, valor):
super().__init__()
self.conta = conta
self.valor = valor
def run(self):
self.conta.depositar(self.valor)
def principal():
conta = ContaBancaria()
threads = []
for _ in range(100):
t = TransacaoThread(conta, 1)
threads.append(t)
t.start()
for t in threads:
t.join()
print(f'Saldo final: R${conta.saldo}')
if __name__ == '__main__':
principal()
O resultado pode ser incorreto devido à falta de sincronização. Utilize locks para proteger recursos críticos:
import threading
import time
class ContaBancaria:
def __init__(self):
self.saldo = 0
self.trava = threading.Lock()
def depositar(self, valor):
self.trava.acquire()
try:
novo_saldo = self.saldo + valor
time.sleep(0.01)
self.saldo = novo_saldo
finally:
self.trava.release()
class TransacaoThread(threading.Thread):
def __init__(self, conta, valor):
super().__init__()
self.conta = conta
self.valor = valor
def run(self):
self.conta.depositar(self.valor)
def principal():
conta = ContaBancaria()
threads = []
for _ in range(100):
t = TransacaoThread(conta, 1)
threads.append(t)
t.start()
for t in threads:
t.join()
print(f'Saldo final: R${conta.saldo}')
if __name__ == '__main__':
principal()
O Global Interpreter Lock (GIL) no Python limita a execução paralela de threads em tarefas CPU-bound, mas threads ainda são úteis para I/O-bound.
Comparação: Processos vs. Threads
A eficiência depende do tipo de tarefa e do overhead de troca de contexto. Tarefas CPU-bound beneficiam-se de multiprocessamento, enquanto I/O-bound podem usar multithreading. O Python também suporta corrotinas com async I/O para alta eficiência em operações de I/O.
Casos de Uso
Em aplicações GUI, como com tkinter, threads evitam o bloqueio da interface:
import tkinter as tk
import threading
import time
def iniciar_download():
botao_download.config(state=tk.DISABLED)
t = threading.Thread(target=tarefa_download, daemon=True)
t.start()
def tarefa_download():
time.sleep(10) # Simula download
tk.messagebox.showinfo('Concluído', 'Download finalizado!')
botao_download.config(state=tk.NORMAL)
def mostrar_sobre():
tk.messagebox.showinfo('Sobre', 'Exemplo de multithreading')
janela = tk.Tk()
janela.title('Exemplo com Threads')
janela.geometry('200x100')
botao_download = tk.Button(janela, text='Download', command=iniciar_download)
botao_download.pack(pady=5)
botao_sobre = tk.Button(janela, text='Sobre', command=mostrar_sobre)
botao_sobre.pack(pady=5)
janela.mainloop()
Para tarefas computacionais intensivas, o multiprocessamento pode acelerar o cálculo. Exemplo de soma de números grandes:
import multiprocessing
import time
def somar_intervalo(intervalo, fila_resultado):
total = sum(intervalo)
fila_resultado.put(total)
def principal():
numeros = list(range(1, 100_000_001))
inicio = time.time()
total_sequencial = sum(numeros)
fim = time.time()
print(f'Soma sequencial: {total_sequencial}, Tempo: {fim - inicio:.3f}s')
inicio_paralelo = time.time()
fila = multiprocessing.Queue()
processos = []
tamanho = len(numeros) // 8
for i in range(8):
inicio_idx = i * tamanho
fim_idx = inicio_idx + tamanho if i < 7 else len(numeros)
p = multiprocessing.Process(target=somar_intervalo, args=(numeros[inicio_idx:fim_idx], fila))
processos.append(p)
p.start()
for p in processos:
p.join()
total_paralelo = sum(fila.get() for _ in range(8))
fim_paralelo = time.time()
print(f'Soma paralela: {total_paralelo}, Tempo: {fim_paralelo - inicio_paralelo:.3f}s')
if __name__ == '__main__':
principal()
O multiprocessamento aproveita múltiplos núcleos, reduzindo significativamente o tempo em tarefas distribuídas.