.NET

3 out, 2016

VB .NET – Threads – Deadlock – o conceito, o problema e a resolução

Publicidade

Neste artigo vou apresentar o conceito de deadlock, mostrar um exemplo e também uma maneira de resolver o problema do deadlock. Tudo isso usando a linguagem VB .NET.

Mas o que é deadlock? A tradução de deadlock pode ser: impasse, beco sem saída. No contexto das Threads, o deadlock ocorre na seguinte situação:

Uma Thread, ou segmento ou linha de execução é uma forma de um processo para dividir a si mesmo em duas ou mais tarefas, que podem ser executadas concorrentemente. A linguagem C# suporta a execução paralela de código através do multithreading, onde uma Thread é uma caminho de execução independente que está apto para rodar simultaneamente com outras threads.

Todas as classes relacionadas à segmentação (threading) estão presentes no namespace System.Threading.

Quando dois ou mais processos concorrentes (ou duas threads) estão esperando pelo outro terminar e nenhum deles termina, estamos em um impasse ou deadlock.

vbn_de1

Cenário para ocorrência de um DeadLock

Vamos supor que temos 2 threads:

  1. Thread1
  2. Thread2

E que temos dois recursos:

  1. Recurso1
  2. Recurso2

A Thread1 já adquiriu um bloqueio no Recurso1 e deseja adquirir um bloqueio no Recurso2. Ao mesmo tempo, a Thread2 já adquiriu um bloqueio no Recurso2 e deseja adquirir um bloqueio no Recurso1.

Uma thread fica esperando pela outra liberar o bloqueio e isso nunca acontece. Temos, assim, um impasse; um beco sem saída. Temos um deadlock.

vbn_deadl11

Vou mostrar agora um exemplo prático mostrando uma simulação da ocorrência do deadlock e como resolver o problema.

Recursos usados: Visual Studio Community 2015

Nota: Baixe e use a versão Community 2015 do VS ela é grátis e é equivalente a versão Professional.

Criando a solução no VS Community

Abra o VS Community 2015 e clique em New Project. Selecione a linguagem Visual Basic e o template Console Application e depois informe o nome VBNET_DeadLock e clique no botão OK:

vbn_deadl12

Definindo o código e simulando o deadlock

Vamos definir no início do módulo duas variáveis que serão usadas para adquirir o bloqueio:

Private ObjLockA As Object = New Object()
Private ObjLockB As Object = New Object()

A seguir, no método Main(), inclua o código abaixo:

Sub Main()

        Console.WriteLine("----Exemplo de DeadLock-----")
        Console.WriteLine()
        ' Inicializa a thread com endereço Tarefa1
        Dim thread1 As New Thread(AddressOf Tarefa1)
        ' Inicializa a thread com endereço Tarefa2
        Dim thread2 As New Thread(AddressOf Tarefa2)
        ' Agenda a execução das threads
        thread1.Start()
        thread2.Start()
        thread1.Join()
        thread2.Join()
        ' Esta instrução nunca será executada
        Console.WriteLine("Processamento concluído...")
        Console.ReadKey()
    End Sub

Neste código estamos criando duas threads (thread1 e thread2) e executando o método Tarefa1 e Tarefa2 respectivamente em cada thread.

Vamos definir o código do método Tarefa1:

Private Sub Tarefa1()
        SyncLock ObjLockA
            Console.WriteLine("Tentando adquirir um bloqueio em ObjLockB")
            ' Pausa de 1 segundo
            Thread.Sleep(1000)
            SyncLock ObjLockB
                ' Este bloco nunca será executado
                Console.WriteLine("Tarefa1 - Seção Crítica.")
                ' Acesso ao recurso compartilhado
            End SyncLock
        End SyncLock
End Sub

Neste código usamos o método SyncLock para adquirir um bloqueio exclusivo no objeto ObjLockA para um recurso antes de executar o recurso.

A seguir, damos uma pausa de 1s na execução da thread usando o método Thread.Sleep(1000) e, logo depois, solicitamos a aquisição de um bloqueio no objeto ObjLockB.

Vamos definir o código do método Tarefa2:

Private Sub Tarefa2()
        SyncLock ObjLockB
            Console.WriteLine("Tentando adquirir um bloqueio em ObjLockA")
            SyncLock ObjLockA
                ' Este bloco de código nunca será executado
                Console.WriteLine("Tarefa2 - Seção Crítica")
                 ' Acesso ao recurso compartilhado
            End SyncLock
        End SyncLock
End Sub

Neste código fazemos o mesmo que no código anterior, invertendo os objetos de bloqueio. Neste cenário, ao executarmos o projeto, iremos obter o seguinte resultado:

vbn_deadl13

Temos aqui o deadlock.

A thread1 está esperando que a thread2 libere o bloqueio para o objeto ObjLockB. Por sua vez, a thread2 está esperando que a thread1 libere o bloqueio para o objeto ObjLockA.

O programa ficará ‘congelado’ indefinidamente, esperando pela liberação dos bloqueios que nunca ocorrerá.

Resolvendo o problema do deadlock

Para resolver o problema do deadlock podemos:

  1. Adquirir um bloqueio em um ordem específica;
  2. Usar a classe Mutex (mútuo exclusivo);
  3. Usar o método Monitor.TryEnter().

Neste exemplo vamos usar a classe Monitor, que provê um mecanismo que sincroniza o acesso aos objetos em um programa multitarefa. Ela controla o acesso aos objetos através da concessão de um bloqueio a um objeto para uma única thread.

O bloqueio do objeto fornece a capacidade de restringir o acesso a um bloco de código conhecido como sessão crítica. Enquanto uma thread possuir o bloqueio para o objeto, nenhuma outra thread poderá adquirir o bloqueio.

Vamos usar o método Monitor.TryEnter que possui a seguinte sintaxe:

Monitor.TryEnter(object obj, int milisegundos)

Tenta obter um bloqueio exclusivo para um objeto especificado por número determinado de milisegundos.

Retorno:

  • true – se a thread adquiriu o bloqueio
  • false – se a thread não conseguir adquirir o bloqueio

Usamos os métodos Enter e Exit para marcar o início e o fim de uma sessão crítica de código.

Se a sessão crítica for um conjunto de instrução contíguas, então, o bloqueio adquirido pelo método Enter garante que somente uma única thread pode executar o código.

Neste caso, é recomendado colocar as instruções em um bloco try/catch e usar o método Exit no bloco finally.

Vamos, então, alterar o código do método Tarefa2 conforme abaixo:

Private Sub Tarefa1()
        SyncLock ObjLockA
            Console.WriteLine("Tentando adquirir um bloqueio em ObjLockB")
            ' Pausa de 1 segundo
            Thread.Sleep(1000)
            ' Tenta adquirir um bloqueio por 5 segundos
            If Monitor.TryEnter(ObjLockB, 5000) Then
                Try
                    ' Este bloco nunca será executado
                    Console.WriteLine("Tarefa1 - Seção Crítica.")
                    ' Acesso ao recurso compartilhado
                Finally
                    Monitor.[Exit](ObjLockB)
                End Try
            Else
                Console.WriteLine("Não foi possível adquirir um bloqueio, saindo de Tarefa1...")
            End If
        End SyncLock
    End Sub

Agora, usando o método Monitor.TryEnter(objLockB,5000), tentamos adquirir um bloqueio para o objeto ObjLockB por 5 segundos e se não for possível, liberamos a tentativa de bloqueio usando o método Monitor.Exit(ObjLockB).

Executando o projeto novamente, agora iremos obter o seguinte resultado:

vbn_deadl14

E assim escapamos do deadlock.

Pegue o projeto completo aqui: VBNET_DeadLock.zip

Até o próximo artigo!