Gerenciando Processos Concorrentes: Estratégias e Dicas
Tags: mutex, lock
Published: 2023-11-24 11:51:57

A programação concorrente é uma parte fundamental da computação moderna. À medida que os processadores evoluem e as aplicações se tornam cada vez mais complexas, a capacidade de executar múltiplas threads ou processos simultaneamente se torna essencial para otimizar o desempenho e a responsividade dos sistemas. No entanto, essa abordagem traz consigo o desafio de lidar com a concorrência, na qual várias threads competem por recursos compartilhados. E é aí que entram os conceitos de Mutex e Lock, duas ferramentas cruciais para gerenciar essa concorrência de maneira eficaz. 

Esses recursos desempenham um papel fundamental em cenários nos quais é imperativo assegurar que apenas uma thread execute o recurso, prevenindo assim duplicação. Esse mecanismo de bloqueio é aplicado por um intervalo de tempo definido, o que significa que um método é temporariamente retido por uma thread durante um período determinado com o propósito de garantir que nenhuma outra thread execute o mesmo método simultaneamente. 

Esses conceitos fazem o bloqueio de threads concorrentes tanto em aplicações que possuem uma única instância, quanto em aplicações distribuídas em diversas instâncias como, por exemplo, o uso de clusters.  

Neste artigo, pretendo elucidar alguns conceitos e oferecer orientações para a implementação de Mutex e Lock. 

## Tempo de Expiração e Base de dados

A base de dados é responsável por guardar os dados dos processos que estão bloqueados. Por exemplo, quando for pagar um boleto, deve ser gravado no banco uma chave com os dados desse boleto para indicar que esse boleto está sendo processado. E, ao fazer isso, é preciso indicar por quanto tempo esse recurso será bloqueado. 

O recomendado é utilizar uma base de dados que já possua um recurso automático de expiração de dados, já que os dados nela são voláteis. Além disso, essa base de dados deve ser rápida para leitura e gravação. 

Recomendo a utilização do Redis, e, nele, gravar um json com os seguintes dados: 

```
"registro": {
        "key": "lib_lock:appName:Environment:IdBoleto",
        "threadId": 0,
        "hostName": "",
        "processName": "",
        "processId": 0,
        "nameOf": "",
        "dataCriacao": "",
        "tempoExpiracao": "",
        "tempoRestanteExpiracao": ""
  }
```

O redis é um banco de **chave** e **valor**. A **chave** será a mesma que está no json citado anteriormente, e o **valor** será todo o json citado.

### Chave

O controle de bloqueio é feito através de uma chave, que contém dados necessários para distinguir processos, ambientes e o registro que está sendo bloqueado. 

Assim, garantimos que, quando gravarmos uma chave na base de dados, esse registro não será processado novamente sem que a chave expire ou seja deletada da base de dados. 

O ideal é uma padronização de uma chave, podemos fazer da seguinte maneira:

- Chave Lock

>lib_lock:NomeAplicacao:Environment:Boleto-11111

- Chave Mutex

>lib_mutex_queue:NomeAplicacao:Environment:Boleto-11111:HostaName:ThreadId:ProcessId

Como o Mutex faz controle de fila das threads, é necessário ter mais dados na chave para facilitar o trabalho de distinguir os itens na fila. 

O atributo Boleto-1111 que está na chave informa o tipo do dado, que nesse caso é um boleto, e mostra o identificador único do registro, que nesse caso é 1111. Já os demais dados podem ser preenchidos da forma que achar mais adequada para o seu projeto. 

## Mutex

O termo "Mutex" é a abreviação de "Mutual Exclusion", o que significa exclusão mútua. Essencialmente, um Mutex é um mecanismo de sincronização usado para garantir que apenas uma thread por vez tenha acesso a um recurso compartilhado. Uma das funcionalidades mais valiosas do Mutex é a sua capacidade de enfileirar threads concorrentes.

### Fluxo

O funcionamento geral do Mutex é ilustrado na imagem abaixo.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z511wl4gqz3shbi8iv2j.png)

### WaitOne

É necessário que o Mutex tenha o recurso de WaitOne, que é responsável por gravar na base de dados que um determinado recurso está bloqueado e ficar travado até que chegue a sua vez de processar o recurso. 

O WaitOne é responsável também por realizar a sincronia da fila no redis.

### Processo Lógico de Mutex

Na imagem abaixo, temos o processo sugerido para fazer um controle de Mutex por meio do WaitOne e garantir que tenha um enfileiramento de threads:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c5avpr2m78cvvjjn1y86.png)

## Lock

O "Lock" representa outra abordagem para controlar threads concorrentes. Nesse cenário, o "Lock" bloqueia um processo por um período definido, e todas as futuras threads que tentarem acessar o mesmo recurso já bloqueado serão encerradas. 

A principal diferença entre o "Mutex" e o "Lock" reside no fato de que o "Mutex" enfileira todas as threads que tentam acessar um recurso já bloqueado e, à medida que essas threads são processadas, permite que a próxima thread seja executada. Por outro lado, o "Lock" encerra todas as threads concorrentes, permitindo que apenas uma única thread seja executada. 

### Fluxo

O funcionamento geral do Lock é ilustrado na imagem abaixo.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gmvvb6q4rsrrg7w30mzf.png)

### AcquireLock

Essa funcionalidade tem a responsabilidade de registrar o processo que está sendo bloqueado na base de dados. Caso o processo já esteja bloqueado, deve gerar uma exceção. 

Ao contrário do "WaitOne", essa funcionalidade não cria nem gerencia filas, ela apenas encerra a requisição. 

## ReleaseLock

O Mutex e o Lock devem ambos possuir a funcionalidade de "ReleaseLock", que tem a responsabilidade de desbloquear o processo na base de dados. 

O desbloqueio do processo na base de dados pode ser alcançado de duas maneiras: por meio da expiração do dado na base de dados (caso esteja sendo utilizado o Redis) ou simplesmente removendo-o da base de dados usando a função "ReleaseLock". 

Por exemplo, ao concluir o processamento de um boleto, o "ReleaseLock" será invocado, resultando na remoção da chave correspondente a esse boleto na base de dados. 

## KeepLock e/ou DelayMutex

Este recurso pode ser bastante útil, pois permitirá estender o período de bloqueio, caso seja necessário. Por exemplo, se houver algum trecho de código que, no pior cenário, possa resultar em longos atrasos, pode ser necessário ajustar a duração do bloqueio do registro no banco de dados. Esse recurso pode ser denominado como "KeepLock" quando se refere ao bloqueio de Lock, e "DelayMutex" quando se relaciona ao Mutex. 

# Monitoramento

É fundamental contar com um sistema de monitoramento para os logs gerados por todos os recursos de Mutex e lock, bem como monitorar a base de dados onde os dados de bloqueio são registrados. Isso permite garantir a estabilidade da base de dados, identificar possíveis quedas e evitar bloqueios prolongados de dados. 

Recomendamos que você e sua equipe de arquitetos avaliem qual é a ferramenta de monitoramento mais adequada para o seu cenário. 

# Conclusão

Os recursos mencionados são de suma importância para sistemas que necessitam assegurar que determinados recursos não sejam processados múltiplas vezes ou simultaneamente. 

Vale ressaltar que há diversas maneiras de implementar um Mutex ou Lock em seu projeto. Contudo, desde que sigam padrões estabelecidos e garantam a confiabilidade das execuções concorrentes de threads, evitando a duplicação de processamento de recursos, sua aplicação se tornará mais confiável, reduzindo custos desnecessários.