Impasse C#

Resumo : neste tutorial, você aprenderá sobre o impasse do C#, como ele ocorre e como corrigi-lo.

Introdução ao impasse C#

Um deadlock acontece quando dois ou mais threads estão bloqueando e esperando um pelo outro para liberar os bloqueios necessários para prosseguir.

Particularmente, cada thread mantém um bloqueio que o outro thread precisa e nenhum deles pode prosseguir até adquirir o bloqueio que está esperando.

Por exemplo, suponha que você tenha dois threads t1 e t2. Cada thread mantém um bloqueio em um recurso compartilhado diferente r1 e r2.

O thread t1 precisa acessar r2 para prosseguir enquanto o thread t2 precisa acessar r1. Ambos os threads tentam adquirir os recursos ao mesmo tempo. Eles estão esperando e bloqueando um ao outro. Como resultado, ocorre um impasse.

O programa a seguir demonstra como ocorre um cenário de deadlock quando dois threads completam vários bloqueios:

using static System.Console;


var lock1 = new object();
var lock2 = new object();

void AcquireLocks1()
{
    var threadId = Thread.CurrentThread.ManagedThreadId;

    lock (lock1)
    {
        WriteLine($"Thread {threadId} acquired lock 1.");
        Thread.Sleep(1000);
        WriteLine($"Thread {threadId} attempted to acquire lock 2.");
        lock (lock2)
        {
            WriteLine($"Thread {threadId} acquired lock 2.");
        }
    }
}

void AcquireLocks2()
{
    var threadId = Thread.CurrentThread.ManagedThreadId;

    lock (lock2)
    {
        WriteLine($"Thread {threadId} acquired lock 2.");
        Thread.Sleep(1000);
        WriteLine($"Thread {threadId} attempted to acquire lock 1.");
        lock (lock1)
        {
            WriteLine($"Thread {threadId} acquired lock 1.");
        }
    }
}

// Create two new threads that compete for the locks
var thread1 = new Thread(AcquireLocks1);
var thread2 = new Thread(AcquireLocks2);

// Start the threads
thread1.Start();
thread2.Start();

// Wait for the threads to complete
thread1.Join();
thread2.Join();

WriteLine("Finished.");
ReadLine();Linguagem de código:  C#  ( cs )

Como funciona.

Primeiro, crie dois objetos de bloqueio lock1 e lock2:

var lock1 = new object();
var lock2 = new object();Linguagem de código:  C#  ( cs )

Segundo, defina o AcquireLock1método que adquire lock1, atrasa um segundo e tenta adquirir lock2:

void AcquireLocks1()
{
    var threadId = Thread.CurrentThread.ManagedThreadId;

    lock (lock1)
    {
        WriteLine($"Thread {threadId} acquired lock 1.");
        Thread.Sleep(1000);
        WriteLine($"Thread {threadId} attempted to acquire lock 2.");
        lock (lock2)
        {
            WriteLine($"Thread {threadId} acquired lock 2.");
        }
    }
}Linguagem de código:  C#  ( cs )

Terceiro, defina AcquireLock2()o método que adquire lock2, atrasa um segundo e tenta adquirir lock1:

void AcquireLocks2()
{
    var threadId = Thread.CurrentThread.ManagedThreadId;

    lock (lock2)
    {
        WriteLine($"Thread {threadId} acquired lock 2.");
        Thread.Sleep(1000);
        WriteLine($"Thread {threadId} attempted to acquire lock 1.");
        lock (lock1)
        {
            WriteLine($"Thread {threadId} acquired lock 1.");
        }
    }
}Linguagem de código:  C#  ( cs )

Quarto, crie dois threads, inicie-os e espere que sejam concluídos:

// Create two new threads that compete for the locks
var thread1 = new Thread(AcquireLocks1);
var thread2 = new Thread(AcquireLocks2);

// Start the threads
thread1.Start();
thread2.Start();

// Wait for the threads to complete
thread1.Join();
thread2.Join();Linguagem de código:  C#  ( cs )

Saída:

Thread 10 acquired lock 1.
Thread 11 acquired lock 2.
Thread 10 attempted to acquire lock 2.
Thread 11 attempted to acquire lock 1.Linguagem de código:  C#  ( cs )

Thread1 executa o método AcquireLock1 enquanto thread2 executa o AcquireLock2método.

Como o thread1 está mantendo o lock1, o thread2 está impedido de adquirir o lock1. Portanto, thread2 está aguardando que thread1 libere o lock1.

Enquanto isso, thread2 está mantendo o lock2 e thread1 está impedido de adquirir o lock1. O thread1 está aguardando o thread2 liberar o lock2.

Ambos os threads estão esperando um pelo outro para obter os bloqueios correspondentes antes de prosseguir, o que resulta em um impasse.

Como resultado, o programa irá travar indefinidamente e você nunca verá a mensagem “Concluído” no console.

Resolvendo um impasse

Para corrigir um impasse, você pode usar uma das seguintes técnicas:

  • Evite manter um bloqueio por muito tempo: quando um thread mantém um bloqueio por muito tempo, pode causar um conflito. Para evitá-lo, você precisa liberar o bloqueio assim que não precisar dele.
  • Evite bloqueios aninhados: é difícil gerenciar bloqueios aninhados. E uma das causas comuns de impasse é o uso de bloqueios aninhados. Se você precisar usar vários bloqueios, tente adquiri-los em uma ordem fixa para evitar impasses.
  • Use um tempo limite: o tempo limite pode ajudar a evitar a ocorrência de um conflito. Se um thread estiver aguardando um bloqueio por um determinado tempo que exceda o tempo limite, você poderá lançar uma exceção ou executar outras ações.
  • Use a técnica de programação assíncrona: a programação assíncrona permite que vários threads sejam executados simultaneamente sem bloquear uns aos outros, o que ajuda a evitar conflitos.

O programa a seguir demonstra como usar um tempo limite para corrigir um impasse no programa acima:

using static System.Console; 

var lock1 = new object();
var lock2 = new object();

void AcquireLocks1()
{
    var threadId = Thread.CurrentThread.ManagedThreadId;

    while (true)
    {
        if (Monitor.TryEnter(lock1, TimeSpan.FromSeconds(1)))
        {
            try
            {
                WriteLine($"Thread {threadId} acquired lock 1.");
                Thread.Sleep(1000);
                WriteLine($"Thread {threadId} attempted to acquire lock 2.");

                if (Monitor.TryEnter(lock2, TimeSpan.FromSeconds(1)))
                {
                    try
                    {
                        WriteLine($"Thread {threadId} acquired lock 2.");
                        break; // exit the loop if both locks are acquired
                    }
                    finally
                    {
                        Monitor.Exit(lock2);
                    }
                }
            }
            finally
            {
                Monitor.Exit(lock1);
            }
        }
    }

    WriteLine($"Thread {threadId} is done.");
}

void AcquireLocks2()
{
    var threadId = Thread.CurrentThread.ManagedThreadId;

    while (true)
    {
        if (Monitor.TryEnter(lock2, TimeSpan.FromSeconds(1)))
        {
            try
            {
                WriteLine($"Thread {threadId} acquired lock 2.");
                Thread.Sleep(1000);
                WriteLine($"Thread {threadId} attempted to acquire lock 1.");

                if (Monitor.TryEnter(lock1, TimeSpan.FromSeconds(1)))
                {
                    try
                    {
                        WriteLine($"Thread {threadId} acquired lock 1.");
                        break; // exit the loop if both locks are acquired
                    }
                    finally
                    {
                        Monitor.Exit(lock1);
                    }
                }
            }
            finally
            {
                Monitor.Exit(lock2);
            }
        }
    }

    WriteLine($"Thread {threadId} is done.");
}

// Create two new threads that compete for the locks
var thread1 = new Thread(AcquireLocks1);
var thread2 = new Thread(AcquireLocks2);

// Start the threads
thread1.Start();
thread2.Start();

// Wait for the threads to complete
thread1.Join();
thread2.Join();

WriteLine("Finished.");
ReadLine();Linguagem de código:  C#  ( cs )

Neste programa, utilizamos o Monitor.TryEnter()para tentar adquirir um bloqueio com tempo limite de um segundo.

Se um thread não conseguir adquirir o bloqueio em um segundo, ele libera o bloqueio e tenta novamente.

O loop while continua tentando até que ambos os threads possam adquirir os bloqueios, o que interrompe o loop e os threads completam suas execuções.

Resumo

  • Um deadlock ocorre quando dois ou mais threads estão aguardando e bloqueando um ao outro para liberar bloqueios.

Deixe um comentário

O seu endereço de email não será publicado. Campos obrigatórios marcados com *