Race Conditions e Deadlocks: conceitos e exemplos.
**Por Ricardo Costa Val do Rosário
Condições de corrida
- Uma condição de corrida ocorre quando dois ou mais processos ou threads acessam dados compartilhados
simultaneamente e o resultado final depende da ordem de execução.
- Essa imprevisibilidade pode levar a corrupção de dados, resultados inconsistentes ou falhas no sistema.
- Características das condições de corrida:
1. Ocorre quando vários processos ou threads acessam dados ou recursos ao mesmo tempo. É dependente do tempo, já que o
resultado varia de acordo com a ordem e o momento de execução, e é não determinístico, pois os resultados são imprevisíveis
e difíceis de reproduzir.
2. Cenários comuns incluem operações de leitura-modificação-gravação, onde um processo lê um valor, modifica-o e grava de volta
sem a devida sincronização.
3. Pode acontecer na condição conhecida como Verificar e Agir quando um processo verifica uma condição e age com base nela, mas a
condição muda entre essas etapas.
4. Descreve-se ainda as Corridas de Inicialização, situação na qual vários threads tentam inicializar um recurso compartilhado ao
mesmo tempo.
Exemplo: Transação de conta bancária
- Considere um cenário em que dois threads estão atualizando simultaneamente o saldo de uma conta bancária:
python
balance = 1000
def withdraw(amount):
global balance
if balance >= amount:
# Simulate some processing time
time.sleep(0.1)
balance -= amount
print(f"Withdrawal successful. New balance: {balance}")
else:
print("Insufficient funds")
- Thread 1
thread1 = threading.Thread(target=withdraw, args=(800,))
- Thread 2
thread2 = threading.Thread(target=withdraw, args=(500,))
thread1.start()
thread2.start()
thread1.join()
thread2.join()
- Aqui ambos os threads podem passar na verificação de saldo inicial, levando a um saldo final incorreto ou cheque especial.
Prevenção de condições de corrida
1. Exclusão Mútua:
Use bloqueios ou semáforos para garantir que apenas um thread possa acessar recursos compartilhados por vez.
2. Operações atômicas:
Utilize instruções atômicas suportadas por hardware para operações simples.
3. Estruturas de dados thread-safe:
Empregue estruturas de dados projetadas para acesso simultâneo.
4. Objetos imutáveis:
Use objetos imutáveis para eliminar a necessidade de sincronização em alguns casos.
Deadlocks
- Ocorre quando dois ou mais processos ficam impossibilitados de prosseguir porque cada um aguarda que o outro
libere um recurso, gerando uma dependência circular e. por fim, a inatividade do sistema indefinidamente.
Condições de Coffinan
- É necessária a coexistência das mesmas para a ocorrência dos Deadlocks. São elas:
1. Exclusão Mútua:
Ao menos um recurso precisa estar em modo não compartilhável.
2. Segure e Espere:
Um processo deve manter pelo menos um recurso enquanto solicita outros recursos ocupados por outros
processos.
3. Sem Preempção:
Os recursos não podem ser retirados à força do processo; devem ser liberados voluntariamente.
4. Espera Circular:
Existe uma cadeia circular composta por dois ou mais processos em que cada um aguarda um recurso ocupado
pelo próximo processo da cadeia.
Exemplo: Problema dos filósofos do jantar
- O problema clássico dos filósofos do jantar demonstra o conceito de deadlock.
python
import threading
import time
class Philosopher(threading.Thread):
def __init__(self, name, left fork, right_fork):
threading.Thread.__init__(self)
self.name = name
self.left_fork = left_fork
self.right_fork = right_fork
def run(self):
while True:
print(f"{self.name} is thinking")
time.sleep(1)
print(f"{self.name} is hungry")
self.left_fork.acquire()
print(f"{self.name} picked up left fork")
self.right_fork.acquire()
print(f"{self.name} picked up right fork and is eating")
time.sleep(1)
self.right_fork.release()
self.left_fork.release()
# Create 5 forks (locks)
forks = [threading.Lock() for _ in range(5)]
# Create 5 philosophers
philosophers = [
Philosopher(f"Philosopher {i}", forks[i], forks[(i + 1) % 5])
for i in range(5)
]
# Start the philosophers
for philosopher in philosophers:
philosopher.start()
- Nesse cenário, caso todos os filósofos escolham simultaneamente a bifurcação à esquerda, ocorre um impasse,
pois todos aguardam indefinidamente pela bifurcação à direita.