Solucionado (ver solução)
Solucionado
(ver solução)
3
respostas

<Artigo Original> Race Conditions e Deadlocks: conceitos e exemplos.

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.
3 respostas
solução!

Estratégias de prevenção de impasses

1. Ordenação de recursos: 
Imponha uma ordenação total aos tipos de recursos e exija que os processos solicitem recursos em 
ordem crescente de enumeração.

2. Mecanismo de tempo limite: 
Defina um tempo limite para solicitações de recursos para evitar espera indefinida.

3. Limite de alocação de recursos: 
Limite o número de recursos que podem ser alocados a qualquer momento.

4. Detecção e recuperação de deadlock: 
Verifique periodicamente se há deadlocks e tome medidas corretivas quando detectado.

Algoritmo do banqueiro

- O Banker's Algorithm é um algoritmo de prevenção de impasse que decide se deve alocar
recursos com base no estado atual do sistema:

Variáveis de estado:

1. Disponível: 
Vetor de comprimento m indicando os recursos disponíveis
2. Max: 
Matriz n x m definindo a demanda máxima de cada processo
3. Alocação: 
Matriz n x m definindo os recursos atualmente alocados
4. Necessidade: 
Matriz n x m indicando as necessidades de recursos restantes

Algoritmo de segurança:

1. Encontre um processo cujas necessidades possam ser satisfeitas com os recursos disponíveis
2. Adicionar os recursos alocados do processo ao pool disponível
3. Marcar o processo como concluído
4. Repita até que todos os processos sejam concluídos ou nenhuma sequência segura seja encontrada

Algoritmo de solicitação de recursos:

1. Verifique se a solicitação excede a reivindicação máxima do processo
2. Verifique se a solicitação excede os recursos disponíveis
3. Alocar provisoriamente os recursos solicitados
4. Execute o algoritmo de segurança
5. Se for seguro, prossiga com a alocação; caso contrário, reverta e aguarde

Detecção de deadlock

- A detecção de deadlock ocorre por meio da avaliação periódica do estado do sistema por meio de:
1.Usando uma variante do Algoritmo do Banqueiro;
2. Construindo e analisando um gráfico de alocação de recursos.

Práticas recomendadas para programação simultânea

1. Use construções de sincronização de alto nível: 
Prefira usar primitivos de sincronização de nível mais alto, como semáforos, monitores 
ou Dat simultâneos uma estrutura sobre eclusas de baixo nível.

2. Minimizar estado compartilhado: 
Reduza a quantidade de dados compartilhados entre threads para minimizar o potencial de condições 
de corrida.
    
3. Siga o princípio do menor privilégio: 
Os threads de concessão acessam apenas os recursos de que precisam absolutamente.
    
4. Implemente o tratamento adequado de erros: 
Projete seus programas simultâneos para lidar e se recuperar de erros de sincronização normalmente.

5. Use bibliotecas thread-safe: 
Utilize bibliotecas bem testadas e thread-safe para operações comuns em ambientes simultâneos

6. Empregando ferramentas de análise estática:
Use ferramentas que possam detectar possíveis condições de corrida e deadlocks em seu código.
    
7. Pratique a programação defensiva: 
Sempre suponha que condições de corrida e deadlocks possam ocorrer e projete seu código de acordo.

8. Protocolos de sincronização de documentos: 
Documente claramente os protocolos de sincronização usados em sua base de código para evitar mal-entendidos 
e erros.

Considerações Finais

1. Condições de corrida e impasses são desafios fundamentais na programação simultânea e no design do 
sistema operacional. 

2. Ao entender suas causas, características e estratégias de prevenção, os desenvolvedores podem criar sistemas 
concorrentes mais robustos e eficientes. 

3. Técnicas de sincronização adequadas, gerenciamento cuidadoso de recursos e adesão às práticas recomendadas 
são essenciais para mitigar esses problemas e garantir a confiabilidade de aplicativos e sistemas operacionais multithread.

4. À medida que a programação simultânea continua a crescer em importância com o surgimento de processadores multi-core 
e sistemas distribuídos, dominar esses conceitos torna-se cada vez mais crucial para engenheiros de software e designers de sistemas. 

5. Ao aplicar os princípios e técnicas discutidos nesta lição, os desenvolvedores podem criar sistemas simultâneos que não são apenas 
eficientes, mas também confiáveis e livres das armadilhas de condições de corrida e impasses.

Oi, Ricardo! Como vai?

Gostei muito da forma como você explicou condições de corrida e deadlocks, trazendo exemplos práticos como o da conta bancária e o dos filósofos jantando. Sua resposta ficou bem completa e mostra que você compreendeu os conceitos com profundidade. É ótimo ver você conectando teoria e prática dessa forma.

Continue compartilhando essas explicações detalhadas, pois ajudam bastante quem está começando no tema. Agradeço por compartilhar suas reflexões com a comunidade Alura.

Conte com o apoio do Fórum na sua jornada. Abraços e bons estudos!

Alura Conte com o apoio da comunidade Alura na sua jornada. Abraços e bons estudos!

Rafaela, tem sido uma grande alegria acompanhar minha evolução em algo que, há menos de seis meses, eu nem imaginava que existia. Méritos também para a Alura.