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.
Cenário para ocorrência de um DeadLock
Vamos supor que temos 2 threads:
- Thread1
- Thread2
E que temos dois recursos:
- Recurso1
- 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.
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:
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:
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:
- Adquirir um bloqueio em um ordem específica;
- Usar a classe Mutex (mútuo exclusivo);
- 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:
E assim escapamos do deadlock.
Pegue o projeto completo aqui: VBNET_DeadLock.zip
Até o próximo artigo!