O Módulo `threading` no Python

O Módulo `threading` no Python

Este artigo explica o módulo threading no Python.

YouTube Video

O Módulo threading no Python

O módulo threading no Python é uma biblioteca padrão que suporta programação multithread. O uso de threads permite que vários processos sejam executados simultaneamente, o que pode melhorar o desempenho dos programas, especialmente em casos que envolvem operações bloqueantes, como espera de I/O. Devido ao Global Interpreter Lock (GIL) do Python, a eficácia do multithreading é limitada para operações dependentes de CPU, mas funciona eficientemente para operações dependentes de I/O.

As seções a seguir explicam os fundamentos do uso do módulo threading e como controlar threads.

Uso Básico de Threads

Criando e Executando Threads

Para criar uma thread e executar processamento concorrente, use a classe threading.Thread. Especifique a função alvo para criar uma thread e executá-la.

 1import threading
 2import time
 3
 4# Function to be executed in a thread
 5def worker():
 6    print("Worker thread started")
 7    time.sleep(2)
 8    print("Worker thread finished")
 9
10# Create and start the thread
11thread = threading.Thread(target=worker)
12thread.start()
13
14# Processing in the main thread
15print("Main thread continues to run")
16
17# Wait for the thread to finish
18thread.join()
19print("Main thread finished")
  • Neste exemplo, a função worker é executada em uma thread separada, enquanto a thread principal continua a operar. Chamando o método join(), a thread principal espera que a sub-thread termine.

Nomeando Threads

Dar nomes significativos às threads facilita o registro e a depuração. Você pode especificar isso com o argumento name.

 1import threading
 2import time
 3
 4# Function to be executed in a thread
 5def worker():
 6    print("Worker thread started")
 7    time.sleep(2)
 8    print("Worker thread finished")
 9
10t = threading.Thread(
11    target=worker,
12    args=("named-worker", 0.3),
13    name="MyWorkerThread"
14)
15
16t.start()
17
18print("Active threads:", threading.active_count())
19for th in threading.enumerate():
20    print(" -", th.name)
21
22t.join()
  • threading.enumerate() retorna uma lista das threads atuais, o que é útil para depuração e monitoramento do estado.

Herdando a Classe Thread

Se você quiser personalizar a classe de execução de threads, pode definir uma nova classe herdando a classe threading.Thread.

 1import threading
 2import time
 3
 4# Inherit from the Thread class
 5class WorkerThread(threading.Thread):
 6    def __init__(self, name, delay, repeat=3):
 7        super().__init__(name=name)
 8        self.delay = delay
 9        self.repeat = repeat
10        self.results = []
11
12    def run(self):
13        for i in range(self.repeat):
14            msg = f"{self.name} step {i+1}"
15            print(msg)
16            self.results.append(msg)
17            time.sleep(self.delay)
18
19# Create and start the threads
20t1 = WorkerThread("Worker-A", delay=0.4, repeat=3)
21t2 = WorkerThread("Worker-B", delay=0.2, repeat=5)
22
23t1.start()
24t2.start()
25
26t1.join()
27t2.join()
28
29print("Results A:", t1.results)
30print("Results B:", t2.results)
  • Neste exemplo, o método run() é sobrescrito para definir o comportamento da thread, permitindo que cada thread mantenha seus próprios dados. Isso é útil quando as threads realizam processamento complexo ou quando você deseja que cada thread tenha seus próprios dados independentes.

Sincronização Entre Threads

Quando várias threads acessam recursos compartilhados simultaneamente, podem ocorrer condições de corrida de dados. Para evitar isso, o módulo threading fornece vários mecanismos de sincronização.

Trava (Lock)

O objeto Lock é usado para implementar o controle exclusivo de recursos entre threads. Enquanto uma thread trava um recurso, outras threads não podem acessar esse recurso.

 1import threading
 2
 3lock = threading.Lock()
 4shared_resource = 0
 5
 6def worker():
 7    global shared_resource
 8    with lock:  # Acquire the lock
 9        local_copy = shared_resource
10        local_copy += 1
11        shared_resource = local_copy
12
13threads = [threading.Thread(target=worker) for _ in range(5)]
14
15for t in threads:
16    t.start()
17
18for t in threads:
19    t.join()
20
21print(f"Final value of shared resource: {shared_resource}")
  • Neste exemplo, cinco threads acessam um recurso compartilhado, mas o Lock é usado para evitar que várias threads modifiquem os dados simultaneamente.

Trava Reentrante (RLock)

Se a mesma thread precisar adquirir uma trava várias vezes, use uma RLock (trava reentrante). Isso é útil para chamadas recursivas ou para chamadas de biblioteca que podem adquirir travas em diferentes chamadas.

 1import threading
 2
 3rlock = threading.RLock()
 4shared = []
 5
 6def outer():
 7    with rlock:
 8        shared.append("outer")
 9        inner()
10
11def inner():
12    with rlock:
13        shared.append("inner")
14
15t = threading.Thread(target=outer)
16t.start()
17t.join()
18print(shared)
  • Com RLock, a mesma thread pode readquirir uma trava que já possui, o que ajuda a evitar deadlocks em aquisições de travas aninhadas.

Condição (Condition)

Condition é usado para que as threads aguardem até que uma condição específica seja atendida. Quando uma thread satisfaz uma condição, você pode chamar notify() para notificar outra thread, ou notify_all() para notificar todas as threads que estão aguardando.

Abaixo está um exemplo de produtor e consumidor usando uma Condition.

 1import threading
 2
 3condition = threading.Condition()
 4shared_data = []
 5
 6def producer():
 7    with condition:
 8        shared_data.append(1)
 9        print("Produced an item")
10        condition.notify()  # Notify the consumer
11
12def consumer():
13    with condition:
14        condition.wait()  # Wait until the condition is met
15
16        item = shared_data.pop(0)
17        print(f"Consumed an item: {item}")
18
19# Create the threads
20producer_thread = threading.Thread(target=producer)
21consumer_thread = threading.Thread(target=consumer)
22
23consumer_thread.start()
24producer_thread.start()
25
26producer_thread.join()
27consumer_thread.join()
  • Este código utiliza uma Condition para que o produtor notifique quando os dados são adicionados, e o consumidor espere por essa notificação antes de obter os dados, alcançando a sincronização.

Daemonização de Threads

Threads daemon são encerradas forçadamente quando a thread principal termina. Enquanto as threads normais precisam aguardar para terminar, as threads daemon terminam automaticamente.

 1import threading
 2import time
 3
 4def worker():
 5    while True:
 6        print("Working...")
 7        time.sleep(1)
 8
 9# Create a daemon thread
10thread = threading.Thread(target=worker)
11thread.daemon = True  # Set as a daemon thread
12
13thread.start()
14
15# Processing in the main thread
16time.sleep(3)
17print("Main thread finished")
  • Neste exemplo, a thread worker é daemonizada, portanto, é encerrada forçadamente quando a thread principal termina.

Gerenciamento de Threads com ThreadPoolExecutor

Além do módulo threading, você pode usar o ThreadPoolExecutor do módulo concurrent.futures para gerenciar um pool de threads e executar tarefas em paralelo.

 1from concurrent.futures import ThreadPoolExecutor
 2import time
 3
 4def worker(seconds):
 5    print(f"Sleeping for {seconds} second(s)")
 6    time.sleep(seconds)
 7    return f"Finished sleeping for {seconds} second(s)"
 8
 9with ThreadPoolExecutor(max_workers=3) as executor:
10    futures = [executor.submit(worker, i) for i in range(1, 4)]
11    for future in futures:
12        print(future.result())
  • ThreadPoolExecutor cria um pool de threads e processa tarefas de forma eficiente. Especifique o número de threads para executar em simultâneo com max_workers.

Comunicação de Eventos Entre Threads

Usando threading.Event, você pode configurar sinalizadores entre threads para notificar outras threads sobre a ocorrência de um evento.

 1import threading
 2import time
 3
 4event = threading.Event()
 5
 6def worker():
 7    print("Waiting for event to be set")
 8    event.wait()  # Wait until the event is set
 9
10    print("Event received, continuing work")
11
12thread = threading.Thread(target=worker)
13thread.start()
14
15time.sleep(2)
16print("Setting the event")
17event.set()  # Set the event and notify the thread
  • Este código demonstra um mecanismo onde a thread de trabalho espera pelo sinal do Event e retoma o processamento quando a thread principal chama event.set().

Tratamento de Exceções e Finalização de Threads

Quando exceções ocorrem em threads, elas não são propagadas diretamente para a thread principal, portanto é necessário um padrão para capturá-las e compartilhá-las.

 1import threading
 2import queue
 3
 4def worker(err_q):
 5    try:
 6        raise ValueError("Something bad")
 7    except Exception as e:
 8        err_q.put(e)
 9
10q = queue.Queue()
11t = threading.Thread(target=worker, args=(q,))
12t.start()
13t.join()
14if not q.empty():
15    exc = q.get()
16    print("Worker raised:", exc)
  • Colocando exceções em uma Queue e recuperando-as na thread principal, você pode detectar falhas de forma confiável. Se você usar concurrent.futures.ThreadPoolExecutor, exceções são relançadas com future.result(), facilitando o tratamento.

O GIL (Global Interpreter Lock) e seus Efeitos

Devido ao mecanismo do GIL (Global Interpreter Lock) no CPython, múltiplos bytecodes Python na verdade não são executados simultaneamente dentro do mesmo processo. Para tarefas que exigem muito do CPU, como cálculos pesados, recomenda-se usar o multiprocessing. Por outro lado, para tarefas limitadas por I/O, como leitura de arquivos ou comunicação de rede, threading funciona de forma eficaz.

Resumo

Usando o módulo threading do Python, você pode implementar programas multithread e executar vários processos simultaneamente. Com mecanismos de sincronização como Lock e Condition, você pode acessar recursos compartilhados com segurança e realizar sincronizações complexas. Além disso, usando threads daemon ou ThreadPoolExecutor, o gerenciamento de threads e o processamento paralelo eficiente tornam-se mais fáceis.

Você pode acompanhar o artigo acima usando o Visual Studio Code em nosso canal do YouTube. Por favor, confira também o canal do YouTube.

YouTube Video