Introdução às Primitivas de Sincronização
As primitivas de sincronização são mecanismos fundamentais em sistemas operacionais e programação concorrente, desenhadas para coordenar a sequência de operações e o acesso a recursos compartilhados entre múltiplos threads ou processos. Seu objetivo primordial é prevenir problemas comuns da concorrência, como condições de corrida, impasses (deadlocks) e inconsistência de dados.
A seguir, listamos algumas das primitivas de sincronização mais comuns:
- Fechos (Locks): Um fecho é uma primitiva básica que controla o acesso a um recurso compartilhado. Somente um thread ou processo pode possuir o fecho em um dado momento. Outros que tentarem adquiri-lo deverão esperar. Fechos são cruciais para evitar acesso simultâneo e garantir a integridade dos dados.
- Variáveis de Condição (Condition Variables): Mais avançadas, variáveis de condição facilitam a coordenação complexa entre threads. Elas permitem que um thread aguarde até que uma determinada condição seja verdadeira, sendo então notificado por outro thread.
- Semáforos (Semaphores): Semáforos são primitivas de sincronização baseadas em contagem, usadas para limitar o número de threads ou processos que podem acessar simultaneamente um recurso. Um contador interno é decrementado ao adquirir e incrementado ao liberar o semáforo.
- Mutexes (Mutual Exclusion): Semelhantes a fechos, mutexes garantem acesso exclusivo a uma seção crítica de código. São frequentemente utilizados em nível de sistema operacional para assegurar que apenas uma entidade possa modificar um dado recurso por vez.
- Barreiras (Barriers): Uma barreira é uma primitiva que força múltiplos threads ou processos a esperarem em um ponto específico de execução até que todos os participantes tenham chegado. Após todos alcançarem a barreira, eles podem prosseguir simultaneamente.
Essas ferramentas são a base para gerenciar a coordenação e as condições de corrida em programas concorrentes, assegurando sua execução correta.
Explorando Fechos (Locks)
Fechos são usados para controlar o acesso a recursos compartilhados, garantindo que apenas um thread ou processo o acesse por vez. Um fecho pode estar em dois estados: travado (locked) ou destravado (unlocked).
Para acessar um recurso protegido por um fecho, um thread deve primeiro tentar adquiri-lo. A aquisição só é bem-sucedida se o fecho estiver destravado. Se o fecho já estiver sendo mantido por outra entidade, o thread solicitante será bloqueado até que o fecho seja liberado.
As operações fundamentais de um fecho são:
adquirir(blocking=True, timeout=None): Tenta obter o fecho. Se disponível, o fecho é travado e o método retornaTrue. Se o fecho estiver ocupado, o comportamento depende dos parâmetrosblockingetimeout, podendo bloquear o thread até que o fecho seja liberado ou retornar imediatamente.liberar(): Libera o fecho, mudando seu estado para destravado. Se houver threads esperando, um deles será acordado e adquirirá o fecho.
A principal aplicação de fechos é em cenários de concorrência (multithreading ou multiprocessing) para proteger o acesso a recursos compartilhados, prevenindo condições de corrida e inconsistências de dados. No entanto, o uso de fechos inevitavelmente introduz uma penalidade de desempenho, aumentando o tempo de execução devido à serialização do acesso.
Categorias de Fechos na Programação Concorrente
Na programação concorrente, diversas variações de fechos atendem a necessidades específicas:
- Fechos de Exclusão Mútua (Mutex): O tipo mais básico, assegura que apenas um thread possa manter o fecho por vez. Impede que múltiplos threads acessem um recurso compartilhado simultaneamente. Em Python,
threading.Lockemultiprocessing.Lockimplementam mutexes. - Fechos Recursivos (RLock): Permitem que o mesmo thread adquira o fecho múltiplas vezes sem se auto-bloquear, prevenindo impasses em código recursivo ou aninhado. Cada aquisição requer uma liberação correspondente.
threading.RLockemultiprocessing.RLocksão exemplos em Python. - Fechos de Variável de Condição (Condition Lock): Combinam um fecho com uma variável de condição para coordenação complexa. Permitem que threads esperem por uma condição específica e sejam notificados quando ela for atendida. Implementado por
threading.Conditionemultiprocessing.Condition. - Semáforos (Semaphore): Um fecho contador que limita o número de threads que podem acessar um recurso simultaneamente. Mantém um contador que é decrementado na aquisição e incrementado na liberação.
threading.Semaphoreemultiprocessing.Semaphoresão as implementações em Python. - Fechos de Evento (Event Lock): Uma primitiva de sincronização para comunicação entre threads. Um thread pode sinalizar um evento, e outros threads podem esperar por ele.
threading.Eventemultiprocessing.Eventoferecem essa funcionalidade.
A escolha do tipo de fecho depende da necessidade específica do problema de concorrência, desde a simples exclusão mútua até a coordenação complexa de threads.
Fechos de Exclusão Mútua em Ação
Fechos para Processos
Utilizando multiprocessing.Lock:
import os
import time
from multiprocessing import Process, Lock
def executar_tarefa(controle_acesso):
# Adquire o fecho para o processo
controle_acesso.acquire()
pid_atual = os.getpid()
print(f"Processo {pid_atual} está executando a tarefa.")
time.sleep(0.1) # Simula trabalho
print(f"Processo {pid_atual} concluiu a tarefa.")
# Libera o fecho
controle_acesso.release()
if __name__ == '__main__':
# Declara um objeto de fecho
bloqueio_processo = Lock()
# Cria uma lista de processos
lista_processos = [Process(target=executar_tarefa, args=(bloqueio_processo,)) for _ in range(3)]
# Inicia todos os processos
for proc in lista_processos:
proc.start()
# Aguarda a conclusão de todos os processos
for proc in lista_processos:
proc.join()
print("Processo principal finalizado!")
A saída mostrará que os processos executam sequencialmente, garantindo que apenas um processo por vez acesse a seção crítica.
Fechos para Threads
Similarmente, com threading.Lock:
import os
import time
from threading import Thread, Lock
def executar_operacao(mutex_thread):
# Adquire o fecho para o thread
mutex_thread.acquire()
id_processo = os.getpid() # Threads compartilham o mesmo PID
nome_thread = threading.current_thread().name # Melhor para identificar threads
print(f"Thread {nome_thread} (PID: {id_processo}) está executando.")
time.sleep(0.1) # Simula trabalho
print(f"Thread {nome_thread} (PID: {id_processo}) terminou a execução.")
# Libera o fecho
mutex_thread.release()
if __name__ == '__main__':
# Declara um objeto de fecho
bloqueio_threads = Lock()
# Cria uma lista de threads
lista_threads = [Thread(target=executar_operacao, args=(bloqueio_threads,)) for _ in range(3)]
# Inicia todas as threads
for t in lista_threads:
t.start()
# Aguarda a conclusão de todas as threads
for t in lista_threads:
t.join()
print("Programa principal finalizado!")
Resolvendo Condições de Corrida em Acesso a Arquivos com Mutexes
Quando múltiplos processos tentam modificar o mesmo arquivo simultaneamente, podem ocorrer condições de corrida, levando a dados corrompidos ou inconsistentes. Considere um sistema de compra de ingressos:
Cenário Sem Fecho (Problema)
import json
import os
import time
from multiprocessing import Process
ARQUIVO_INGRESSOS = os.path.join(os.path.dirname(__file__), 'ingressos.json')
def ler_dados(caminho_arquivo):
with open(caminho_arquivo, mode='r', encoding='utf8') as f_obj:
dados = json.load(f_obj)
return dados
def salvar_dados(caminho_arquivo, dados_atuais):
with open(caminho_arquivo, mode='w', encoding='utf8') as f_obj:
json.dump(dados_atuais, f_obj, ensure_ascii=False, indent=4)
def comprar_ingresso_sem_bloqueio():
print("Verificando disponibilidade de ingressos...")
time.sleep(0.05) # Simula delay
dados_atuais = ler_dados(ARQUIVO_INGRESSOS)
quantidade = dados_atuais.get("total_ingressos")
print(f"[{os.getpid()}] Ingressos disponíveis: {quantidade}")
if quantidade <= 0:
print(f"[{os.getpid()}] Ingressos esgotados!")
else:
print(f"[{os.getpid()}] Compra bem-sucedida!")
dados_atuais["total_ingressos"] -= 1
salvar_dados(ARQUIVO_INGRESSOS, dados_atuais)
if __name__ == '__main__':
# Inicializa o arquivo de ingressos com 2 disponíveis
estado_inicial = {"total_ingressos": 2}
salvar_dados(ARQUIVO_INGRESSOS, estado_inicial)
print("Iniciando simulação de compra sem fecho...")
clientes = [Process(target=comprar_ingresso_sem_bloqueio) for _ in range(5)]
for cliente in clientes:
cliente.start()
for cliente in clientes:
cliente.join()
print("Simulação sem fecho concluída. Verifique 'ingressos.json'.")
Neste cenário, múltiplos processos podem ler o mesmo valor de ingressos simultaneamente antes que qualquer um deles tenha a chance de decrementar e salvar. Isso leva a um problema de "supervenda" onde mais ingressos do que o disponível são vendidos.
Cenário Com Fecho (Solução)
import json
import os
import time
from multiprocessing import Process, Lock
ARQUIVO_INGRESSOS_BLOQUEADO = os.path.join(os.path.dirname(__file__), 'ingressos_bloqueados.json')
def ler_dados_bloqueados(caminho_arquivo):
with open(caminho_arquivo, mode='r', encoding='utf8') as f_obj:
dados = json.load(f_obj)
return dados
def salvar_dados_bloqueados(caminho_arquivo, dados_atuais):
with open(caminho_arquivo, mode='w', encoding='utf8') as f_obj:
json.dump(dados_atuais, f_obj, ensure_ascii=False, indent=4)
def comprar_ingresso_com_bloqueio(mutex_compra):
mutex_compra.acquire() # Adquire o fecho
try:
print(f"[{os.getpid()}] Verificando disponibilidade de ingressos...")
time.sleep(0.05) # Simula delay
dados_atuais = ler_dados_bloqueados(ARQUIVO_INGRESSOS_BLOQUEADO)
quantidade = dados_atuais.get("total_ingressos")
print(f"[{os.getpid()}] Ingressos disponíveis: {quantidade}")
if quantidade <= 0:
print(f"[{os.getpid()}] Ingressos esgotados!")
else:
print(f"[{os.getpid()}] Compra bem-sucedida!")
dados_atuais["total_ingressos"] -= 1
salvar_dados_bloqueados(ARQUIVO_INGRESSOS_BLOQUEADO, dados_atuais)
finally:
mutex_compra.release() # Libera o fecho, mesmo se ocorrer um erro
if __name__ == '__main__':
# Inicializa o arquivo de ingressos com 2 disponíveis
estado_inicial = {"total_ingressos": 2}
salvar_dados_bloqueados(ARQUIVO_INGRESSOS_BLOQUEADO, estado_inicial)
# Declara um objeto de fecho para proteger o arquivo
bloqueio_compra = Lock()
print("\nIniciando simulação de compra COM fecho...")
clientes_protegidos = [Process(target=comprar_ingresso_com_bloqueio, args=(bloqueio_compra,)) for _ in range(5)]
for cliente in clientes_protegidos:
cliente.start()
for cliente in clientes_protegidos:
cliente.join()
print("Simulação COM fecho concluída. Verifique 'ingressos_bloqueados.json'.")
Com o fecho, cada processo adquire o bloqueio antes de ler, decrementar e salvar, garantindo exclusão mútua e prevenindo a supervenda.
Impasse (Deadlock)
from threading import Thread, Lock
import time
lock_recurso_a = Lock()
lock_recurso_b = Lock()
class OperacaoConcorrente(Thread):
def run(self):
self.executar_parte1()
self.executar_parte2()
def executar_parte1(self):
lock_recurso_a.acquire()
print(f'{self.name} adquiriu o Recurso A')
time.sleep(0.1) # Pequeno atraso para expor o impasse
lock_recurso_b.acquire()
print(f'{self.name} adquiriu o Recurso B')
lock_recurso_b.release()
lock_recurso_a.release()
def executar_parte2(self):
lock_recurso_b.acquire()
print(f'{self.name} adquiriu o Recurso B (na parte 2)')
time.sleep(0.1) # Pequeno atraso para expor o impasse
lock_recurso_a.acquire()
print(f'{self.name} adquiriu o Recurso A (na parte 2)')
lock_recurso_a.release()
lock_recurso_b.release()
def iniciar_impasses():
for i in range(2): # Apenas 2 threads para demonstrar o impasse claramente
t = OperacaoConcorrente()
t.start()
if __name__ == '__main__':
iniciar_impasses()
# A saída mostrará que as threads podem travar, por exemplo:
# Thread-1 adquiriu o Recurso A
# Thread-2 adquiriu o Recurso B (na parte 2)
# Neste ponto, Thread-1 precisa de B, e Thread-2 precisa de A, resultando em impasse.
Um impasse (deadlock) é uma situação de bloqueio mútuo entre dois ou mais threads/processos, onde cada um espera por um recurso que está sendo mantido por outro, criando um ciclo de espera infinita. Isso paralisa o programa.
Condições Necessárias para Impasses:
- Exclusão Mútua: Um recurso só pode ser usado por um thread/processo por vez.
- Retenção e Espera: Um thread/processo detém um recurso enquanto espera por outro.
- Não Preempção: Recursos não podem ser tomados à força de um thread/processo; só podem ser liberados voluntariamente.
- Espera Circular: Existe um ciclo onde cada thread/processo no ciclo está esperando por um recurso detido pelo próximo thread/processo no ciclo.
Estratégias para Evitar Impasses:
- Ordem Fixa de Aquisição de Fechos: Garanta que todos os threads adquiram os fechos na mesma ordem predefinida.
- Mecanismos de Timeout: Ao tentar adquirir um fecho, defina um tempo limite. Se o fecho não for obtido dentro desse período, o thread pode abortar ou tentar novamente.
- Evitar Fechos Aninhados: Minimize a prática de adquirir um fecho enquanto já se detém outro, reduzindo a chance de "retenção e espera".
- Detecção e Recuperação de Impasses: Implementar algoritmos que detectem impasses e tomem ações para quebrá-los (como preempção ou reinício de processos).
Fechos Recursivos (RLock)
Um fecho recursivo, ou reentrante, permite que o mesmo thread o adquira múltiplas vezes sem bloquear a si mesmo. Ele mantém uma contagem interna; cada chamada a acquire() incrementa a contagem e cada release() a decrementa. O fecho só é liberado para outros threads quando a contagem chega a zero.
RLocks são ideais para situações onde uma função que já detém um fecho precisa chamar outra função que também tenta adquirir o mesmo fecho (código recursivo ou aninhado).
Em Python, threading.RLock implementa essa funcionalidade.
from threading import Thread, RLock
import time
# Ambos os nomes de fecho apontam para o mesmo objeto RLock
fecho_recurso_X = RLock()
fecho_recurso_Y = fecho_recurso_X # fecho_recurso_Y é uma referência ao mesmo RLock
class ThreadComRLock(Thread):
def run(self):
self.metodo_a()
self.metodo_b()
def metodo_a(self):
fecho_recurso_X.acquire()
print(f'{self.name} adquiriu RLock X.')
fecho_recurso_Y.acquire() # O mesmo thread pode adquirir novamente
print(f'{self.name} adquiriu RLock Y (recursivamente).')
fecho_recurso_Y.release()
fecho_recurso_X.release()
def metodo_b(self):
fecho_recurso_Y.acquire()
print(f'{self.name} adquiriu RLock Y (no método b).')
time.sleep(0.05) # Pequeno atraso
fecho_recurso_X.acquire() # O mesmo thread pode adquirir novamente
print(f'{self.name} adquiriu RLock X (no método b recursivamente).')
fecho_recurso_X.release()
fecho_recurso_Y.release()
def executar_com_rlocks():
for i in range(3):
t = ThreadComRLock()
t.start()
if __name__ == '__main__':
executar_com_rlocks()
Variáveis de Condição
Uma variável de condição (threading.Condition em Python) combina um fecho com a capacidade de threads esperarem por uma condição específica e serem notificados quando essa condição for atendida. É ideal para coordenação complexa de threads, como em problemas de produtor-consumidor.
Analisando a implementação, é possível especificar o tipo de fecho subjacente (Lock ou RLock). Se nenhum for fornecido, um RLock é criado por padrão.
Métodos Chave:
acquire(self, \*args): Adquire o fecho subjacente.release(self): Libera o fecho subjacente.wait(self, timeout=None): Libera o fecho subjacente e bloqueia o thread até ser notificado ou até o timeout expirar. Ao ser notificado, readquire o fecho antes de retornar.notify(self, n=1): Acorda aténthreads que estão esperando na variável de condição.notify\_all(self): Acorda todos os threads que estão esperando na variável de condição.
Exemplo Prático (Produtor-Consumidor):
import threading
import time
import random
# Recurso compartilhado: uma fila simples
fila_itens = []
CAPACIDADE_FILA = 5
# Cria uma variável de condição
condicao_fila = threading.Condition()
def funcao_produtor():
global fila_itens
for i in range(1, 11): # Produz 10 itens
with condicao_fila:
# Espera se a fila estiver cheia
while len(fila_itens) == CAPACIDADE_FILA:
print(f"Produtor: Fila cheia. Aguardando para produzir...")
condicao_fila.wait() # Libera o fecho e espera
item_novo = f"Item-{i}"
fila_itens.append(item_novo)
print(f"Produtor: Produziu {item_novo}. Fila: {fila_itens}")
condicao_fila.notify_all() # Notifica consumidores
time.sleep(random.uniform(0.5, 1.5))
def funcao_consumidor():
global fila_itens
for _ in range(10): # Consome 10 itens
with condicao_fila:
# Espera se a fila estiver vazia
while not fila_itens:
print(f"Consumidor: Fila vazia. Aguardando por itens...")
condicao_fila.wait() # Libera o fecho e espera
item_consumido = fila_itens.pop(0)
print(f"Consumidor: Consumiu {item_consumido}. Fila: {fila_itens}")
condicao_fila.notify_all() # Notifica produtores
time.sleep(random.uniform(0.5, 1.5))
if __name__ == "__main__":
produtor_thread = threading.Thread(target=funcao_produtor, name="ProdutorThread")
consumidor_thread = threading.Thread(target=funcao_consumidor, name="ConsumidorThread")
produtor_thread.start()
consumidor_thread.start()
produtor_thread.join()
consumidor_thread.join()
print("Simulação de Produtor-Consumidor finalizada.")
Semáforos
Semáforos são mecanismos de sincronização que controlam o acesso a um conjunto limitado de recursos. Eles são compostos por um contador e operações atômicas que manipulam esse contador. O contador de um semáforo representa o número de acessos disponíveis ao recurso.
Operações Básicas:
- P (ou
acquire()): Decrementa o contador. Se o contador se tornar negativo, o thread é bloqueado. - V (ou
release()): Incrementa o contador. Se houver threads bloqueados, um deles é acordado.
Em Python, threading.Semaphore permite criar semáforos. O valor inicial do semáforo (value) define a capacidade máxima de acessos simultâneos. Se não especificado, o valor padrão é 1, tornando-o funcionalmente similar a um mutex.
Métodos Chave:
threading.Semaphore(value=1): Inicializa o semáforo com um contador.acquire(blocking=True, timeout=None): Tenta decrementar o contador. Se o contador for > 0, ele é decrementado e a função retorna. Caso contrário, o thread bloqueia até que o contador seja > 0 ou até o timeout.release(): Incrementa o contador do semáforo. Acorda um thread se houver algum esperando.
Exemplo de Semáforo:
import threading
import time
# Cria um semáforo com capacidade para 2 acessos simultâneos
sem = threading.Semaphore(2)
def processo_trabalhador():
sem.acquire() # Tenta adquirir uma "permissão"
nome_thread = threading.current_thread().name
print(f"{nome_thread} iniciou a tarefa (slot adquirido).")
time.sleep(random.uniform(0.5, 1.5)) # Simula trabalho
print(f"{nome_thread} concluiu a tarefa (slot liberado).")
sem.release() # Libera a "permissão"
# Cria 5 threads que tentarão acessar um recurso limitado a 2 por vez
threads_trabalhadoras = [threading.Thread(target=processo_trabalhador, name=f"Trab-{i}") for i in range(5)]
for t in threads_trabalhadoras:
t.start()
for t in threads_trabalhadoras:
t.join()
print("Todos os trabalhadores finalizaram.")
É também possível usar semáforos com a sintaxe with:
import threading
import time
import random
# Cria um semáforo com capacidade para 2 acessos simultâneos
sem_contexto = threading.Semaphore(2)
def processo_com_contexto():
with sem_contexto: # Adquire e libera automaticamente
nome_thread = threading.current_thread().name
print(f"{nome_thread} iniciou a tarefa (via 'with').")
time.sleep(random.uniform(0.5, 1.5))
print(f"{nome_thread} concluiu a tarefa (via 'with').")
threads_contexto = [threading.Thread(target=processo_com_contexto, name=f"Contexto-{i}") for i in range(5)]
for t in threads_contexto:
t.start()
for t in threads_contexto:
t.join()
print("Todos os trabalhadores com contexto finalizaram.")
Semáforos com ThreadPoolExecutor
Combinar semáforos com um pool de threads é útil para limitar o acesso a um recurso específico, mesmo que o pool tenha mais threads do que a capacidade do recurso.
import threading
import time
from concurrent.futures import ThreadPoolExecutor
import random
def tarefa_com_recurso_limitado(sem_recurso, id_recurso):
with sem_recurso: # Controla o acesso ao recurso
nome_thread = threading.current_thread().name
print(f"[{nome_thread}] Adquiriu slot para recurso {id_recurso}.")
time.sleep(random.uniform(0.5, 1.0)) # Simula uso do recurso
print(f"[{nome_thread}] Usou e liberou recurso {id_recurso}.")
return f"Recurso {id_recurso} processado."
if __name__ == "__main__":
# Cria um semáforo que permite 2 acessos simultâneos ao "recurso"
semaforo_recursos = threading.Semaphore(value=2)
# Cria um pool de threads com 3 trabalhadores
with ThreadPoolExecutor(max_workers=3) as executor:
# Submete 5 tarefas. O pool pode rodar 3 threads, mas o semáforo limita o recurso a 2.
submissoes = [executor.submit(tarefa_com_recurso_limitado, semaforo_recursos, f"DADO-{i}") for i in range(5)]
for futuro in submissoes:
print(futuro.result())
print("Todas as tarefas com pool e semáforo concluídas.")
Fechos de Evento (Event)
Um evento (threading.Event) é uma ferramenta de comunicação entre threads. Ele permite que um thread sinalize a ocorrência de um evento, e outros threads esperem por esse sinal. Um evento tem um flag interno que pode estar "set" (verdadeiro) ou "clear" (falso).
Operações:
set(): Define o flag interno como verdadeiro, acordando todos os threads que estão esperando.clear(): Define o flag interno como falso, fazendo com que futuras chamadas await()bloqueiem.wait(timeout=None): Bloqueia até que o flag interno seja verdadeiro. Se o flag já for verdadeiro, retorna imediatamente.
Eventos são geralmente "uma vez", no sentido de que um set() acordará todos os wait() atuais e futuros até que clear() seja chamado. Para comunicação contínua baseada em condições, Condition pode ser mais apropriado.
Exemplo Básico de Evento:
import threading
import time
import random
# Cria um objeto Event
meu_evento = threading.Event()
def trabalhador_evento(nome_trab):
print(f"[{nome_trab}] Aguardando o sinal...")
meu_evento.wait() # Bloqueia até que meu_evento.set() seja chamado
print(f"[{nome_trab}] Sinal recebido! Prosseguindo...")
# Faz algum trabalho após o sinal
time.sleep(random.uniform(0.1, 0.5))
print(f"[{nome_trab}] Concluiu sua parte.")
# Cria vários threads trabalhadores
threads_evento = [threading.Thread(target=trabalhador_evento, args=(f"Trab-{i}",)) for i in range(3)]
for t in threads_evento:
t.start()
# O thread principal espera um pouco e então sinaliza o evento
time.sleep(2)
print("\n=== Thread Principal: Enviando o sinal! ===\n")
meu_evento.set() # Sinaliza o evento, acordando os trabalhadores
for t in threads_evento:
t.join()
print("\nTodos os threads finalizaram após o evento.")
Jogo da Esconde-Esconde com Eventos:
import random
import threading
import time
sinal_jogo = threading.Event()
def jogador_esconderijo():
nome_jogador = threading.current_thread().name
print(f"[{nome_jogador}]: Estou me escondendo!")
time.sleep(random.uniform(0.5, 1.0))
print(f"[{nome_jogador}]: Escondido! Sinalizando para começar a procurar.")
sinal_jogo.set() # Sinaliza que está pronto
sinal_jogo.wait() # Espera o procurador sinalizar o fim
print(f"[{nome_jogador}]: Fim de jogo para o esconderijo!")
def jogador_procurador():
nome_jogador = threading.current_thread().name
print(f"[{nome_jogador}]: Início do jogo! Vou esperar você se esconder.")
sinal_jogo.wait() # Espera o esconderijo sinalizar
print(f"[{nome_jogador}]: Você se escondeu? Ok, estou procurando!")
time.sleep(random.uniform(1.0, 2.0))
achou = random.choice([True, False])
if achou:
print(f"[{nome_jogador}]: Te encontrei!")
else:
print(f"[{nome_jogador}]: Não te encontrei desta vez.")
sinal_jogo.set() # Sinaliza o fim da procura
if __name__ == '__main__':
esconderijo_t = threading.Thread(target=jogador_esconderijo, name="Esconderijo")
procurador_t = threading.Thread(target=jogador_procurador, name="Procurador")
procurador_t.start()
time.sleep(0.1) # Garante que o procurador comece a esperar primeiro
esconderijo_t.start()
esconderijo_t.join()
procurador_t.join()
print("\nJogo da Esconde-Esconde Encerrado.")
Global Interpreter Lock (GIL)
O GIL (Global Interpreter Lock) é um mecanismo presente na implementação CPython do interpretador Python que garante que apenas um thread por vez possa executar bytecode Python. Ele foi introduzido para simplificar a implementação do CPython e proteger suas estruturas de dados internas de acessos concorrentes por múltiplos threads.
Pontos Chave sobre o GIL:
- Natureza Global: O GIL é um fecho global que bloqueia todo o interpretador, impedindo que múltiplos threads de Python executem código Python simultaneamente.
- Específico do CPython: O GIL é uma característica da implementação CPython. Outros interpretadores Python (como Jython ou IronPython) não possuem GIL, e algumas implementações alternativas de Python estão explorando formas de remover ou desativar o GIL.
- Impacto no Desempenho Multi-threading: Para tarefas intensivas em CPU, o GIL limita severamente o ganho de desempenho em aplicações multi-threaded em sistemas com múltiplos núcleos, pois apenas um núcleo pode estar ativamente executando código Python por vez.
- Menor Impacto em Tarefas I/O-Intensivas: O GIL é liberado automaticamente durante operações de I/O de longa duração (como leitura/escrita de arquivos, requisições de rede,
time.sleep()). Isso permite que outros threads executem código Python enquanto o primeiro thread aguarda a conclusão da operação de I/O. Por essa razão, multi-threading em Python ainda é eficaz para aplicações I/O-bound. - Alternativa Multi-processamento: Para tarefas CPU-intensivas que precisam aproveitar múltiplos núcleos, a solução comum em Python é usar multi-processamento (módulos como
multiprocessing). Cada processo tem seu próprio interpretador Python e, portanto, seu próprio GIL, permitindo execução paralela real. - Não é um Substituidor de Fechos Explícitos: Embora o GIL evite algumas condições de corrida de baixo nível (protegendo as estruturas internas do interpretador), ele NÃO substitui a necessidade de fechos explícitos para proteger a lógica da sua aplicação e estruturas de dados personalizadas contra inconsistências lógicas em ambientes multi-threaded.
Historicamente, o GIL facilitou o desenvolvimento do CPython e de muitas extensões C. Para a maioria das aplicações, o gargalo não é o GIL. No entanto, para cargas de trabalho de computação intensiva, é crucial considerar multi-processamento ou mover partes do código para extensões C otimizadas que podem liberar o GIL.
Demonstração do GIL (Observação de Comportamento):
import os
from threading import Thread
import time
contador_global = 50
def diminuir_contador():
global contador_global
# O GIL garante que a operação de decremento 'num -= 1' será atômica
# em CPython, mas isto não é um substituto para fechos lógicos complexos.
temp_val = contador_global # Leitura
time.sleep(0.01) # Pequena pausa para permitir troca de threads (o GIL é liberado e readquirido)
contador_global = temp_val - 1 # Escrita
print(f"[{threading.current_thread().name} | PID: {os.getpid()}] Valor: {contador_global}")
if __name__ == '__main__':
threads_gil = [Thread(target=diminuir_contador, name=f"Thread-{i}") for i in range(5)]
for t in threads_gil:
t.start()
for t in threads_gil:
t.join()
print("=== Simulação GIL concluída ===")
print(f"Valor final do contador global: {contador_global}")
Neste exemplo simples, devido à natureza do GIL em CPython, mesmo sem um fecho explícito, a operação contador\_global -= 1 pode parecer "segura" para múltiplos threads no sentido de que o valor final pode ser o esperado (50 - 5 = 45). Isso ocorre porque o GIL serilaiza a execução do bytecode Python. No entanto, se a operação fosse mais complexa ou envolvesse múltiplos passos que não fossem atômicos no nível do bytecode, um fecho explícito ainda seria necessário para garantir a correção lógica.