Introdução
Dominar como esperar a conclusão de threads Python é essencial para construir aplicações robustas e confiáveis. Em programas multi-threaded, a sincronização adequada garante que as operações sejam concluídas na ordem correta e que os recursos sejam utilizados de forma eficiente.
Neste laboratório, você aprenderá como criar threads Python, esperar que elas sejam concluídas e lidar com múltiplas threads. Essas habilidades são fundamentais para o desenvolvimento de aplicações concorrentes que podem realizar múltiplas tarefas simultaneamente, mantendo a sincronização adequada.
Criando Sua Primeira Thread Python
O módulo threading do Python fornece uma maneira simples de criar e gerenciar threads. Nesta etapa, você aprenderá como criar uma thread básica e observar seu comportamento.
Entendendo Threads
Uma thread é um fluxo de execução separado em um programa. Quando você executa um script Python, ele começa com uma única thread chamada thread principal (main thread). Ao criar threads adicionais, seu programa pode realizar múltiplas tarefas simultaneamente.
Threads são úteis para:
- Executar operações demoradas sem bloquear o programa principal
- Processar tarefas em paralelo para melhorar o desempenho
- Lidar com múltiplas conexões de clientes em uma aplicação servidor
Criando uma Thread Simples
Vamos começar criando um script Python simples que demonstra como criar e iniciar uma thread.
Abra um novo arquivo no editor clicando no menu "File", selecionando "New File" e, em seguida, salvando-o como
simple_thread.pyno diretório/home/labex/project.Adicione o seguinte código ao arquivo:
import threading
import time
def print_numbers():
"""Function that prints numbers from 1 to 5 with a delay."""
for i in range(1, 6):
print(f"Number {i} from thread")
time.sleep(1) ## Sleep for 1 second
## Create a thread that targets the print_numbers function
number_thread = threading.Thread(target=print_numbers)
## Start the thread
print("Starting the thread...")
number_thread.start()
## Main thread continues execution
print("Main thread continues to run...")
print("Main thread is doing other work...")
## Sleep for 2 seconds to demonstrate both threads running concurrently
time.sleep(2)
print("Main thread finished its work!")
Salve o arquivo pressionando
Ctrl+Sou clicando em "File" > "Save".Execute o script abrindo um terminal (se ainda não estiver aberto) e executando:
python3 /home/labex/project/simple_thread.py
Você deve ver uma saída semelhante a esta:
Starting the thread...
Main thread continues to run...
Main thread is doing other work...
Number 1 from thread
Number 2 from thread
Main thread finished its work!
Number 3 from thread
Number 4 from thread
Number 5 from thread
Analisando o que Aconteceu
Neste exemplo:
- Importamos os módulos
threadingetime. - Definimos uma função
print_numbers()que imprime números de 1 a 5 com um atraso de 1 segundo entre cada um. - Criamos um objeto thread, especificando a função a ser executada usando o parâmetro
target. - Iniciamos a thread usando o método
start(). - A thread principal continuou sua execução, imprimindo mensagens e dormindo por 2 segundos.
- Tanto a thread principal quanto nossa thread de números foram executadas simultaneamente, e é por isso que a saída é intercalada.
Observe que a thread principal terminou antes que a thread de números imprimisse todos os seus números. Isso ocorre porque as threads são executadas independentemente e, por padrão, o programa Python sairá quando a thread principal terminar, mesmo que outras threads ainda estejam em execução.
Na próxima etapa, você aprenderá como esperar a conclusão de uma thread usando o método join().
Esperando a Conclusão de uma Thread com join()
Na etapa anterior, você criou uma thread que era executada independentemente da thread principal. No entanto, existem muitas situações em que você precisa esperar que uma thread termine seu trabalho antes de prosseguir com o restante do seu programa. É aqui que o método join() se torna útil.
Entendendo o Método join()
O método join() de um objeto thread bloqueia a thread chamadora (geralmente a thread principal) até que a thread cujo método join() é chamado termine. Isso é essencial quando:
- A thread principal precisa de resultados de uma thread de trabalho
- Você precisa garantir que todas as threads sejam concluídas antes de sair do programa
- A ordem das operações é importante para a lógica da sua aplicação
Criando uma Thread e Esperando sua Conclusão
Vamos modificar nosso exemplo anterior para demonstrar como esperar a conclusão de uma thread usando o método join().
Crie um novo arquivo chamado
join_thread.pyno diretório/home/labex/project.Adicione o seguinte código ao arquivo:
import threading
import time
def calculate_sum(numbers):
"""Function that calculates the sum of numbers with a delay."""
print("Starting the calculation...")
time.sleep(3) ## Simulate a time-consuming calculation
result = sum(numbers)
print(f"Calculation result: {result}")
return result
## Create a list of numbers
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
## Create a thread that targets the calculate_sum function
calculation_thread = threading.Thread(target=calculate_sum, args=(numbers,))
## Start the thread
print("Main thread: Starting the calculation thread...")
calculation_thread.start()
## Do some other work in the main thread
print("Main thread: Doing some other work while waiting...")
for i in range(5):
print(f"Main thread: Working... ({i+1}/5)")
time.sleep(0.5)
## Wait for the calculation thread to complete
print("Main thread: Waiting for the calculation thread to finish...")
calculation_thread.join()
print("Main thread: Calculation thread has finished!")
## Continue with the main thread
print("Main thread: Continuing with the rest of the program...")
- Salve o arquivo e execute-o com o seguinte comando:
python3 /home/labex/project/join_thread.py
Você deve ver uma saída semelhante a esta:
Main thread: Starting the calculation thread...
Starting the calculation...
Main thread: Doing some other work while waiting...
Main thread: Working... (1/5)
Main thread: Working... (2/5)
Main thread: Working... (3/5)
Main thread: Working... (4/5)
Main thread: Working... (5/5)
Main thread: Waiting for the calculation thread to finish...
Calculation result: 55
Main thread: Calculation thread has finished!
Main thread: Continuing with the rest of the program...
A Importância de join()
Neste exemplo:
- Criamos uma thread que realiza um cálculo (somando números).
- A thread principal fez algum outro trabalho simultaneamente.
- Quando a thread principal precisou garantir que o cálculo fosse concluído, ela chamou
calculation_thread.join(). - O método
join()fez com que a thread principal esperasse até que a thread de cálculo terminasse. - Depois que a thread de cálculo foi concluída, a thread principal continuou sua execução.
Este padrão é muito útil quando você precisa garantir que todas as tarefas em threads sejam concluídas antes de prosseguir com o restante do seu programa. Sem join(), a thread principal pode continuar e até mesmo sair antes que as threads de trabalho tenham concluído suas tarefas.
Usando join() com um Timeout
Às vezes, você pode querer esperar por uma thread, mas não indefinidamente. O método join() aceita um parâmetro de timeout opcional que especifica o número máximo de segundos a serem esperados.
Vamos modificar nosso código para demonstrar isso:
Crie um novo arquivo chamado
join_timeout.pyno diretório/home/labex/project.Adicione o seguinte código:
import threading
import time
def long_running_task():
"""A function that simulates a very long-running task."""
print("Long-running task started...")
time.sleep(10) ## Simulate a 10-second task
print("Long-running task completed!")
## Create and start the thread
task_thread = threading.Thread(target=long_running_task)
task_thread.start()
## Wait for the thread to complete, but only for up to 3 seconds
print("Main thread: Waiting for up to 3 seconds...")
task_thread.join(timeout=3)
## Check if the thread is still running
if task_thread.is_alive():
print("Main thread: The task is still running, but I'm continuing anyway!")
else:
print("Main thread: The task has completed within the timeout period.")
## Continue with the main thread
print("Main thread: Continuing with other operations...")
## Let's sleep a bit to see the long-running task complete
time.sleep(8)
print("Main thread: Finished.")
- Salve o arquivo e execute-o:
python3 /home/labex/project/join_timeout.py
A saída deve ser semelhante a esta:
Long-running task started...
Main thread: Waiting for up to 3 seconds...
Main thread: The task is still running, but I'm continuing anyway!
Main thread: Continuing with other operations...
Long-running task completed!
Main thread: Finished.
Neste exemplo, a thread principal espera por até 3 segundos para que a thread de tarefa seja concluída. Como a tarefa leva 10 segundos, a thread principal continua após o timeout, enquanto a thread de tarefa continua sendo executada em segundo plano.
Essa abordagem é útil quando você deseja dar às threads a chance de serem concluídas, mas precisa continuar independentemente após um certo período de tempo.
Trabalhando com Múltiplas Threads
Em aplicações do mundo real, você frequentemente precisa trabalhar com múltiplas threads simultaneamente. Esta etapa ensinará como criar, gerenciar e sincronizar múltiplas threads em Python.
Criando Múltiplas Threads
Ao lidar com múltiplas tarefas semelhantes, é comum criar múltiplas threads para processá-las simultaneamente. Isso pode melhorar significativamente o desempenho, especialmente para operações vinculadas a I/O (I/O-bound), como baixar arquivos ou fazer requisições de rede.
Vamos criar um exemplo que usa múltiplas threads para processar uma lista de tarefas:
Crie um novo arquivo chamado
multiple_threads.pyno diretório/home/labex/project.Adicione o seguinte código:
import threading
import time
import random
def process_task(task_id):
"""Function to process a single task."""
print(f"Starting task {task_id}...")
## Simulate variable processing time
processing_time = random.uniform(1, 3)
time.sleep(processing_time)
print(f"Task {task_id} completed in {processing_time:.2f} seconds.")
return task_id
## List of tasks to process
tasks = list(range(1, 6)) ## Tasks with IDs 1 through 5
## Create a list to store our threads
threads = []
## Create and start a thread for each task
for task_id in tasks:
thread = threading.Thread(target=process_task, args=(task_id,))
threads.append(thread)
print(f"Created thread for task {task_id}")
thread.start()
print(f"All {len(threads)} threads have been started")
## Wait for all threads to complete
for thread in threads:
thread.join()
print("All tasks have been completed!")
- Salve o arquivo e execute-o:
python3 /home/labex/project/multiple_threads.py
A saída variará a cada vez devido aos tempos de processamento aleatórios, mas deve ser semelhante a esta:
Created thread for task 1
Starting task 1...
Created thread for task 2
Starting task 2...
Created thread for task 3
Starting task 3...
Created thread for task 4
Starting task 4...
Created thread for task 5
Starting task 5...
All 5 threads have been started
Task 1 completed in 1.23 seconds.
Task 3 completed in 1.45 seconds.
Task 2 completed in 1.97 seconds.
Task 5 completed in 1.35 seconds.
Task 4 completed in 2.12 seconds.
All tasks have been completed!
Entendendo o Fluxo de Execução
Neste exemplo:
- Definimos uma função
process_task()que simula o processamento de uma tarefa com uma duração aleatória. - Criamos uma lista de IDs de tarefas (1 a 5).
- Para cada tarefa, criamos uma thread, armazenamos em uma lista e a iniciamos.
- Após iniciar todas as threads, usamos um segundo loop com
join()para esperar que cada thread fosse concluída. - Somente após todas as threads serem concluídas, imprimimos a mensagem final.
Este padrão é muito útil quando você tem um lote de tarefas independentes que podem ser processadas em paralelo.
Thread Pool Executors
Para um gerenciamento de threads mais avançado, o módulo concurrent.futures do Python fornece a classe ThreadPoolExecutor. Isso cria um pool de threads de trabalho que podem ser reutilizadas, o que é mais eficiente do que criar e destruir threads para cada tarefa.
Vamos reescrever nosso exemplo usando um thread pool:
Crie um novo arquivo chamado
thread_pool.pyno diretório/home/labex/project.Adicione o seguinte código:
import concurrent.futures
import time
import random
def process_task(task_id):
"""Function to process a single task."""
print(f"Starting task {task_id}...")
## Simulate variable processing time
processing_time = random.uniform(1, 3)
time.sleep(processing_time)
print(f"Task {task_id} completed in {processing_time:.2f} seconds.")
return f"Result of task {task_id}"
## List of tasks to process
tasks = list(range(1, 6)) ## Tasks with IDs 1 through 5
## Create a ThreadPoolExecutor
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
## Submit all tasks and store the Future objects
print(f"Submitting {len(tasks)} tasks to the thread pool with 3 workers...")
future_to_task = {executor.submit(process_task, task_id): task_id for task_id in tasks}
## As each task completes, get its result
for future in concurrent.futures.as_completed(future_to_task):
task_id = future_to_task[future]
try:
result = future.result()
print(f"Got result from task {task_id}: {result}")
except Exception as e:
print(f"Task {task_id} generated an exception: {e}")
print("All tasks have been processed!")
- Salve o arquivo e execute-o:
python3 /home/labex/project/thread_pool.py
A saída variará novamente devido aos tempos de processamento aleatórios, mas deve ser semelhante a esta:
Submitting 5 tasks to the thread pool with 3 workers...
Starting task 1...
Starting task 2...
Starting task 3...
Task 2 completed in 1.15 seconds.
Starting task 4...
Got result from task 2: Result of task 2
Task 1 completed in 1.82 seconds.
Starting task 5...
Got result from task 1: Result of task 1
Task 3 completed in 2.25 seconds.
Got result from task 3: Result of task 3
Task 4 completed in 1.45 seconds.
Got result from task 4: Result of task 4
Task 5 completed in 1.67 seconds.
Got result from task 5: Result of task 5
All tasks have been processed!
Benefícios dos Thread Pools
A abordagem do thread pool oferece várias vantagens:
- Gerenciamento de Recursos: Ele limita o número de threads que podem ser executadas simultaneamente, evitando o esgotamento dos recursos do sistema.
- Agendamento de Tarefas: Ele lida com o agendamento de tarefas automaticamente, iniciando novas tarefas à medida que as threads ficam disponíveis.
- Coleta de Resultados: Ele fornece maneiras convenientes de coletar resultados de tarefas concluídas.
- Tratamento de Exceções: Torna o tratamento de exceções em threads mais direto.
Em nosso exemplo, definimos max_workers=3, o que significa que apenas 3 threads serão executadas de uma vez, embora tenhamos 5 tarefas. À medida que as threads concluem suas tarefas, elas são reutilizadas para as tarefas restantes.
Os thread pools são particularmente úteis quando você tem muito mais tarefas do que deseja que as threads executem simultaneamente, ou quando as tarefas estão sendo geradas continuamente.
Timeouts de Thread e Daemon Threads
Nesta etapa final, você aprenderá sobre dois conceitos importantes no gerenciamento de threads: definir timeouts e usar daemon threads. Essas técnicas oferecem mais controle sobre como as threads se comportam e interagem com o programa principal.
Trabalhando com Timeouts de Thread
Como você aprendeu na Etapa 2, o método join() aceita um parâmetro de timeout. Isso é útil quando você deseja esperar que uma thread seja concluída, mas apenas até um determinado ponto.
Vamos criar um exemplo mais prático onde implementamos uma função que tenta buscar dados com um timeout:
Crie um novo arquivo chamado
thread_with_timeout.pyno diretório/home/labex/project.Adicione o seguinte código:
import threading
import time
import random
def fetch_data(data_id):
"""Simulate fetching data that might take varying amounts of time."""
print(f"Fetching data #{data_id}...")
## Simulate different fetch times, occasionally very long
fetch_time = random.choices([1, 8], weights=[0.8, 0.2])[0]
time.sleep(fetch_time)
if fetch_time > 5: ## Simulate a slow fetch
print(f"Data #{data_id}: Fetch took too long!")
return None
else:
print(f"Data #{data_id}: Fetch completed in {fetch_time} seconds!")
return f"Data content for #{data_id}"
def fetch_with_timeout(data_id, timeout=3):
"""Fetch data with a timeout."""
result = [None] ## Using a list to store result from the thread
def target_func():
result[0] = fetch_data(data_id)
## Create and start the thread
thread = threading.Thread(target=target_func)
thread.start()
## Wait for the thread with a timeout
thread.join(timeout=timeout)
if thread.is_alive():
print(f"Data #{data_id}: Fetch timed out after {timeout} seconds!")
return "TIMEOUT"
else:
return result[0]
## Try to fetch several pieces of data
for i in range(1, 6):
print(f"\nAttempting to fetch data #{i}")
result = fetch_with_timeout(i, timeout=3)
if result == "TIMEOUT":
print(f"Main thread: Fetch for data #{i} timed out, moving on...")
elif result is None:
print(f"Main thread: Fetch for data #{i} completed but returned no data.")
else:
print(f"Main thread: Successfully fetched: {result}")
print("\nAll fetch attempts completed!")
- Salve o arquivo e execute-o:
python3 /home/labex/project/thread_with_timeout.py
A saída variará, mas deve ser semelhante a esta:
Attempting to fetch data #1
Fetching data #1...
Data #1: Fetch completed in 1 seconds!
Main thread: Successfully fetched: Data content for #1
Attempting to fetch data #2
Fetching data #2...
Data #2: Fetch completed in 1 seconds!
Main thread: Successfully fetched: Data content for #2
Attempting to fetch data #3
Fetching data #3...
Data #3: Fetch timed out after 3 seconds!
Main thread: Fetch for data #3 timed out, moving on...
Data #3: Fetch took too long!
Attempting to fetch data #4
Fetching data #4...
Data #4: Fetch completed in 1 seconds!
Main thread: Successfully fetched: Data content for #4
Attempting to fetch data #5
Fetching data #5...
Data #5: Fetch completed in 1 seconds!
Main thread: Successfully fetched: Data content for #5
All fetch attempts completed!
Este exemplo demonstra:
- Uma função que tenta buscar dados e pode ser lenta
- Uma função wrapper que usa threading com um timeout
- Como lidar com timeouts de forma elegante e continuar com outras operações
Entendendo Daemon Threads
Em Python, daemon threads são threads que são executadas em segundo plano. A principal diferença entre daemon threads e threads não-daemon é que o Python não esperará que as daemon threads sejam concluídas antes de sair. Isso é útil para threads que executam tarefas em segundo plano que não devem impedir a saída do programa.
Vamos criar um exemplo para demonstrar daemon threads:
Crie um novo arquivo chamado
daemon_threads.pyno diretório/home/labex/project.Adicione o seguinte código:
import threading
import time
def background_task(name, interval):
"""A task that runs in the background at regular intervals."""
count = 0
while True:
count += 1
print(f"{name}: Iteration {count} at {time.strftime('%H:%M:%S')}")
time.sleep(interval)
def main_task():
"""The main task that runs for a set amount of time."""
print("Main task: Starting...")
time.sleep(5)
print("Main task: Completed!")
## Create two background threads
print("Creating background monitoring threads...")
monitor1 = threading.Thread(target=background_task, args=("Monitor-1", 1), daemon=True)
monitor2 = threading.Thread(target=background_task, args=("Monitor-2", 2), daemon=True)
## Start the background threads
monitor1.start()
monitor2.start()
print("Background monitors started, now starting main task...")
## Run the main task
main_task()
print("Main task completed, program will exit without waiting for daemon threads.")
print("Daemon threads will be terminated when the program exits.")
- Salve o arquivo e execute-o:
python3 /home/labex/project/daemon_threads.py
A saída deve ser semelhante a esta:
Creating background monitoring threads...
Background monitors started, now starting main task...
Main task: Starting...
Monitor-1: Iteration 1 at 14:25:10
Monitor-2: Iteration 1 at 14:25:10
Monitor-1: Iteration 2 at 14:25:11
Monitor-1: Iteration 3 at 14:25:12
Monitor-2: Iteration 2 at 14:25:12
Monitor-1: Iteration 4 at 14:25:13
Monitor-1: Iteration 5 at 14:25:14
Monitor-2: Iteration 3 at 14:25:14
Main task: Completed!
Main task completed, program will exit without waiting for daemon threads.
Daemon threads will be terminated when the program exits.
Neste exemplo:
- Criamos duas daemon threads que são executadas continuamente, imprimindo mensagens em intervalos regulares.
- Definimos
daemon=Trueao criar as threads, o que as marca como daemon threads. - A thread principal é executada por 5 segundos e depois sai.
- Quando a thread principal sai, o programa termina e as daemon threads também são automaticamente terminadas.
Threads Não-Daemon vs. Daemon
Para entender melhor a diferença, vamos criar mais um exemplo que compara daemon e non-daemon threads:
Crie um novo arquivo chamado
daemon_comparison.pyno diretório/home/labex/project.Adicione o seguinte código:
import threading
import time
def task(name, seconds, daemon=False):
"""A task that runs for a specified amount of time."""
print(f"{name} starting {'(daemon)' if daemon else '(non-daemon)'}")
time.sleep(seconds)
print(f"{name} finished after {seconds} seconds")
## Create a non-daemon thread that runs for 8 seconds
non_daemon_thread = threading.Thread(
target=task,
args=("Non-daemon thread", 8, False),
daemon=False ## This is the default, so it's not actually needed
)
## Create a daemon thread that runs for 8 seconds
daemon_thread = threading.Thread(
target=task,
args=("Daemon thread", 8, True),
daemon=True
)
## Start both threads
non_daemon_thread.start()
daemon_thread.start()
## Let the main thread run for 3 seconds
print("Main thread will run for 3 seconds...")
time.sleep(3)
## Check which threads are still running
print("\nAfter 3 seconds:")
print(f"Daemon thread is alive: {daemon_thread.is_alive()}")
print(f"Non-daemon thread is alive: {non_daemon_thread.is_alive()}")
print("\nMain thread is finishing. Here's what will happen:")
print("1. The program will wait for all non-daemon threads to complete")
print("2. Daemon threads will be terminated when the program exits")
print("\nWaiting for non-daemon threads to finish...")
## We don't need to join the non-daemon thread, Python will wait for it
## But we'll explicitly join it for clarity
non_daemon_thread.join()
print("All non-daemon threads have finished, program will exit now.")
- Salve o arquivo e execute-o:
python3 /home/labex/project/daemon_comparison.py
A saída deve ser semelhante a esta:
Non-daemon thread starting (non-daemon)
Daemon thread starting (daemon)
Main thread will run for 3 seconds...
After 3 seconds:
Daemon thread is alive: True
Non-daemon thread is alive: True
Main thread is finishing. Here's what will happen:
1. The program will wait for all non-daemon threads to complete
2. Daemon threads will be terminated when the program exits
Waiting for non-daemon threads to finish...
Non-daemon thread finished after 8 seconds
All non-daemon threads have finished, program will exit now.
Observações chave:
- Ambas as threads iniciam e são executadas simultaneamente.
- Após 3 segundos, ambas as threads ainda estão em execução.
- O programa espera que a thread não-daemon termine (após 8 segundos).
- A thread daemon ainda está em execução quando o programa sai, mas é terminada.
- A thread daemon nunca chega a imprimir sua mensagem de conclusão porque é terminada quando o programa sai.
Quando Usar Daemon Threads
Daemon threads são úteis para:
- Tarefas de monitoramento em segundo plano
- Operações de limpeza
- Serviços que devem ser executados durante a duração do programa, mas não impedi-lo de sair
- Threads de temporizador que acionam eventos em intervalos regulares
Threads não-daemon são apropriadas para:
- Operações críticas que devem ser concluídas
- Tarefas que não devem ser interrompidas
- Operações que devem ser finalizadas de forma limpa antes que o programa saia
Entender quando usar cada tipo é uma parte importante do projeto de aplicações multi-threaded robustas.
Resumo
Neste laboratório, você aprendeu as técnicas essenciais para trabalhar com threads Python e como esperar que elas sejam concluídas. Aqui está um resumo dos principais conceitos abordados:
Criando e Iniciando Threads: Você aprendeu como criar um objeto thread, especificar a função alvo e iniciar sua execução com o método
start().Esperando por Threads com join(): Você descobriu como usar o método
join()para esperar que uma thread seja concluída antes de continuar com o programa principal, garantindo a sincronização adequada.Trabalhando com Múltiplas Threads: Você praticou a criação e o gerenciamento de múltiplas threads, tanto manualmente quanto usando a classe
ThreadPoolExecutorpara um gerenciamento de threads mais eficiente.Timeouts de Thread e Daemon Threads: Você explorou tópicos avançados, incluindo a definição de timeouts para operações de thread e o uso de daemon threads para tarefas em segundo plano.
Essas habilidades fornecem uma base para o desenvolvimento de aplicações multi-threaded em Python. Multi-threading permite que seus programas executem múltiplas tarefas simultaneamente, melhorando o desempenho e a capacidade de resposta, especialmente para operações vinculadas a I/O (I/O-bound).
Ao continuar trabalhando com threads, lembre-se destas melhores práticas:
- Use threads para tarefas vinculadas a I/O, não para tarefas vinculadas a CPU (considere usar multiprocessing para as últimas)
- Esteja atento aos recursos compartilhados e use mecanismos de sincronização apropriados
- Considere usar abstrações de nível superior como
ThreadPoolExecutorpara gerenciar múltiplas threads - Use daemon threads para tarefas em segundo plano que não devem impedir a saída do programa
Com essas habilidades e práticas, você agora está equipado para construir aplicações Python mais eficientes e responsivas usando técnicas de multi-threading.



