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étodojoin(), 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
Conditionpara 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())ThreadPoolExecutorcria um pool de threads e processa tarefas de forma eficiente. Especifique o número de threads para executar em simultâneo commax_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
Evente retoma o processamento quando a thread principal chamaevent.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
Queuee recuperando-as na thread principal, você pode detectar falhas de forma confiável. Se você usarconcurrent.futures.ThreadPoolExecutor, exceções são relançadas comfuture.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.