Processos e Threads em Python: Técnicas de Programação Concorrente

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.

Tags: Python Multiprocessing threading GIL Concorrência

Publicado em 6-16 18:47 por Thomas