Aprenda a usar Threads em Python: Programação concorrente

As Threads em Python são uma poderosa ferramenta de programação que permite executar tarefas simultaneamente. Com o uso de threads, é possível melhorar a eficiência e a responsividade do seu código, assim aproveitando ao máximo os recursos do seu processador. Enfim, vamos explorar o mundo das threads em Python, aprender como utilizá-las e fornecer exemplos práticos para facilitar o seu aprendizado.

O básico das threads

As threads permitem que várias partes do seu código sejam executadas em paralelo, assim aumentando a eficiência do programa. Então, vamos entender os conceitos básicos das threads em Python. Importe o módulo threading para trabalhar com threads. Então veja um exemplo de criação e execução de uma thread:

Python
import threading

def minha_thread():
    print("Olá, sou uma thread!")

# Criando e iniciando a thread
thread = threading.Thread(target=minha_thread)
thread.start()

Entenda bem Método Assíncrono e Síncrono

Antes de mais nada é importante entender bem o que é um Método Assíncrono e Síncrono. Uma das principais características das threads é a sua capacidade de funcionar de forma assíncrona, em contraste com o método síncrono. Enquanto em um método síncrono as instruções são executadas de forma sequencial, uma após a outra, em uma thread podemos ter várias instruções sendo executadas simultaneamente, de maneira independente.

Concorrência entre Threads

As threads permitem que partes do programa sejam executadas de forma concorrente, ou seja, múltiplas linhas de execução ocorrem ao mesmo tempo. Isso traz vantagens significativas quando temos tarefas que podem ser executadas de forma independente, pois acelera a execução do programa e melhora a eficiência.

No entanto, diferentemente dos processos, que são executados de forma independente uns dos outros e possuem sua própria área de memória, as threads compartilham a mesma área de memória do processo pai. Contudo isso significa que as threads podem acessar e modificar as mesmas variáveis, objetos e recursos compartilhados. No entanto, essa característica também exige cuidado e sincronização adequada para evitar condições de corrida e problemas de concorrência.

Sendo assim, quando utilizamos threads, podemos dividir tarefas em partes menores e atribuí-las a diferentes threads. Então cada thread executará sua parte da tarefa de forma independente, aproveitando os benefícios da execução paralela. Isso é especialmente útil em situações em que temos operações demoradas ou bloqueantes, como acesso a bancos de dados, chamadas de rede ou operações intensivas de processamento.

No entanto, ao executar threads, não há uma garantia explícita de qual thread será executada primeiro ou em que ordem as instruções serão executadas. Isso ocorre porque as threads são agendadas pelo sistema operacional e podem ser interrompidas e retomadas em momentos diferentes. Sendo assim essa natureza assíncrona das threads pode trazer desafios adicionais em relação ao controle do fluxo do programa e à sincronização de recursos compartilhados.

Importância dos Mutexes, Semáforos e Locks

Assim, com o uso adequado de mecanismos de sincronização, como mutexes, semáforos e locks, podemos controlar e coordenar o acesso a recursos compartilhados, evitando problemas como condições de corrida e deadlocks.

Em resumo, as threads proporcionam uma maneira eficiente de lidar com tarefas concorrentes, permitindo a execução paralela de partes independentes do programa. Embora essa abordagem assíncrona traz benefícios significativos em termos de desempenho e escalabilidade, é fundamental entender os desafios envolvidos, como sincronização e acesso a recursos compartilhados, a fim de escrever programas robustos e livres de problemas de concorrência.

Afinal, veja um exemplo simples para entender Thread é executada

Python
import threading

# Função que será executada pela thread
def count_numbers(start, end):
    for i in range(start, end+1):
        thread_id = threading.get_ident()
        print(f"Thread {thread_id}: {i}")

# Criação das threads
t1 = threading.Thread(target=count_numbers, args=(1, 50))
t2 = threading.Thread(target=count_numbers, args=(51, 100))

# Início das threads
t1.start()
t2.start()

# Aguarda a conclusão das threads
t1.join()
t2.join()

print("Contagem concluída!")

Sincronização de threads

Quando várias threads estão executando ao mesmo tempo, é importante sincronizá-las para evitar problemas de concorrência. Por exemplo, se duas threads estiverem tentando acessar a mesma variável ao mesmo tempo, podem ocorrer erros. Sendo assim use mecanismos de sincronização, como bloqueios, para evitar condições de corrida. Veja um exemplo:

Python
import threading

contador = 0
bloqueio = threading.Lock()

def incrementar():
    global contador
    with bloqueio:
        contador += 1

# Criando e iniciando as threads
threads = []
for _ in range(10):
    thread = threading.Thread(target=incrementar)
    threads.append(thread)
    thread.start()

# Aguardando a conclusão das threads
for thread in threads:
    thread.join()

print("Valor final do contador:", contador)

Comunicação entre threads

Para usar Threads em Python às vezes é necessário que as threads cooperem ou compartilhem informações entre si. Então utilize filas (queues) para trocar dados entre as threads. Então veja um exemplo:

Python
import threading
import queue

fila = queue.Queue()

def produtor():
    for i in range(5):
        fila.put(i)

def consumidor():
    while not fila.empty():
        item = fila.get()
        print("Consumido:", item)

# Criando e iniciando as threads
thread_produtor = threading.Thread(target=produtor)
thread_consumidor = threading.Thread(target=consumidor)
thread_produtor.start()
thread_consumidor.start()

# Aguardando a conclusão das threads
thread_produtor.join()
thread_consumidor.join()

Tratamento de erros em threads

Lidar com erros em threads pode ser desafiador. Contudo é importante capturar e tratar exceções adequadamente para garantir a estabilidade do programa. Então veja um exemplo de tratamento de exceção em uma thread:

Python
import threading

def minha_thread():
    try:
        # Código da thread
        pass
    except Exception as e:
        print("Erro na thread:", e)

# Criando e iniciando a thread
thread = threading.Thread(target=minha_thread)
thread.start()

Bloqueando Threads com Deadlock

Um deadlock ocorre quando duas ou mais threads ficam bloqueadas permanentemente, assim aguardando uma condição que nunca será satisfeita. Sendo assim isso pode resultar na paralisação do programa e na impossibilidade de continuar a execução. Então vamos propor um exercício para praticar o entendimento e a resolução de deadlocks.

Python
import threading

# Recursos compartilhados
resource_a = threading.Lock()
resource_b = threading.Lock()

def thread_a():
    with resource_a:
        print("Thread A adquiriu o recurso A")
        with resource_b:
            print("Thread A adquiriu o recurso B")
            # Realize as operações necessárias com os recursos

def thread_b():
    with resource_b:
        print("Thread B adquiriu o recurso B")
        with resource_a:
            print("Thread B adquiriu o recurso A")
            # Realize as operações necessárias com os recursos

# Criação das threads
t1 = threading.Thread(target=thread_a)
t2 = threading.Thread(target=thread_b)

# Início das threads
t1.start()
t2.start()

# Aguarda a conclusão das threads
t1.join()
t2.join()

Nesse exercício, temos duas threads que tentam adquirir dois recursos ao mesmo tempo. No entanto, cada thread precisa adquirir ambos os recursos para continuar sua execução. Então o código apresentado não possui uma solução para evitar o deadlock. Então desafie-se a implementar uma solução que garanta que as threads adquiram os recursos em uma ordem específica, evitando o deadlock.

Controle de Acesso

Antes de mais nada, o controle de acesso é fundamental em ambientes concorrentes, onde várias threads competem pelo acesso a recursos compartilhados. Sendo assim, se não houver um controle adequado, isso pode resultar em resultados inconsistentes e condições de corrida. Então vamos propor um exercício para praticar o controle de acesso.

Python
import threading

# Recurso compartilhado
counter = 0
counter_lock = threading.Lock()

def increment_counter():
    with counter_lock:
        global counter
        counter += 1

def worker():
    for _ in range(10000):
        increment_counter()

# Criação das threads
threads = [threading.Thread(target=worker) for _ in range(5)]

# Início das threads
for thread in threads:
    thread.start()

# Aguarda a conclusão das threads
for thread in threads:
    thread.join()

# Exibe o valor final do contador
print("Valor final do contador:", counter)

Mutexes: Controle de Acesso Exclusivo

No entanto, quando falamos de ambientes de programação concorrente, é comum a necessidade de controlar o acesso exclusivo a recursos compartilhados. Um mutex (mutual exclusion) é um mecanismo que permite que apenas uma thread por vez acesse determinado recurso, evitando problemas como condições de corrida.

Para utilizar um mutex, é necessário seguir alguns passos básicos:

  1. Inicializar o mutex: Crie um objeto de mutex e inicialize-o antes de usá-lo.
  2. Adquirir o mutex: Antes de acessar o recurso compartilhado, a thread precisa adquirir o mutex. Caso o mutex esteja bloqueado por outra thread, a thread atual aguardará até que o mutex seja liberado.
  3. Acessar o recurso compartilhado: Após adquirir o mutex, a thread pode realizar operações no recurso compartilhado com segurança.
  4. Liberar o mutex: Após concluir as operações no recurso compartilhado, a thread deve liberar o mutex para permitir que outras threads possam adquiri-lo.

Aqui está um exemplo de código que ilustra o uso de um mutex em Python:

Python
import threading

# Inicializa o mutex
mutex = threading.Lock()

# Função que utiliza o recurso compartilhado
def funcao_com_recurso_compartilhado():
    # Adquire o mutex
    mutex.acquire()
    
    try:
        # Operações no recurso compartilhado
        print("Acessando o recurso compartilhado...")
        # Código da operação
    finally:
        # Libera o mutex
        mutex.release()

# Cria as threads que utilizarão o recurso compartilhado
thread1 = threading.Thread(target=funcao_com_recurso_compartilhado)
thread2 = threading.Thread(target=funcao_com_recurso_compartilhado)

# Inicia as threads
thread1.start()
thread2.start()

# Aguarda a finalização das threads
thread1.join()
thread2.join()

Portanto vimos neste exemplo que o objeto mutex é inicializado utilizando a classe Lock do módulo threading. Assim a função funcao_com_recurso_compartilhado representa o trecho de código em que o recurso compartilhado é utilizado. Antes de acessar o recurso, a thread adquire o mutex utilizando o método acquire(), garantindo o acesso exclusivo. Após concluir as operações, o mutex é liberado através do método release().

Semáforos: Controle de Acesso e Sincronização

Os semáforos são outro mecanismo importante para o controle de acesso e sincronização em programação concorrente. Eles permitem controlar o acesso a recursos compartilhados por um número específico de threads simultaneamente.

Existem dois tipos comuns de semáforos: semáforos binários e semáforos contadores.

  • Semáforos binários: Um semáforo binário pode ter apenas dois valores: 0 e 1. Ele é utilizado para controlar o acesso a um recurso compartilhado, permitindo que apenas uma thread por vez o utilize.
  • Semáforos contadores: Um semáforo contador pode ter um valor maior que 1. Ele é utilizado para controlar o acesso a um recurso compartilhado por um número específico de threads simultaneamente. O valor do semáforo é decrementado quando uma thread o adquire e incrementado quando uma thread o libera.

Aqui está um exemplo de código que demonstra o uso de semáforos em Python:

Python
import threading

# Inicializa o semáforo com valor 2
semaforo = threading.Semaphore(2)

# Função que utiliza o recurso compartilhado
def funcao_com_recurso_compartilhado():
    # Adquire o semáforo
    semaforo.acquire()
    
    try:
        # Operações no recurso compartilhado
        print("Acessando o recurso compartilhado...")
        # Código da operação
    finally:
        # Libera o semáforo
        semaforo.release()

# Cria as threads que utilizarão o recurso compartilhado
thread1 = threading.Thread(target=funcao_com_recurso_compartilhado)
thread2 = threading.Thread(target=funcao_com_recurso_compartilhado)

# Inicia as threads
thread1.start()
thread2.start()

# Aguarda a finalização das threads
thread1.join()
thread2.join()

Conforme mostrado anteriormente, o objeto semaforo é inicializado com o valor 2, assim permitindo que até duas threads acessem o recurso compartilhado simultaneamente. Sobretudo a função funcao_com_recurso_compartilhado representa o trecho de código em que o recurso é utilizado. Antes de acessar o recurso, a thread adquire o semáforo utilizando o método acquire(). Assim, quando uma thread adquire o semáforo, seu valor é decrementado. Após concluir as operações, o semáforo é liberado utilizando o método release(), incrementando seu valor para permitir que outras threads o adquiram.

Conclusão

Enfim, neste guia aprendemos os conceitos básicos das threads em Python e exploramos exemplos práticos de como utilizá-las. Assim com o conhecimento adquirido, você poderá melhorar a eficiência do seu código, executando tarefas simultaneamente e aproveitando ao máximo os recursos do seu processador. Por isso lembre-se de sincronizar corretamente as threads, compartilhar dados de forma segura e tratar erros adequadamente. Agora é sua vez de praticar e explorar ainda mais o potencial das threads em seus projetos!

Sobretudo você poderá consultar a documentação oficial aqui.

Sendo assim, espero que este guia seja útil para você explorar o mundo das threads em Python. No entanto se tiver alguma dúvida, deixe seu comentário abaixo. Boa sorte em sua jornada de aprendizado com threads em Python!

Para aprender mais, veja todos as nossas postagens sobre Python aqui.

Tags: |

Sobre o Autor

Terra
Terra

Apaixonado por tecnologia, trabalha com T.I. e desenvolvimento de softwares desde 1994.

0 Comentários

Deixe um comentário