3 formas de controlar semáforos em AdvPL

Hoje vamos mostrar o que é um semáforo e como controlar via AdvPL.

O artigo abaixo jovem, foi escrito pelo lendário Emerson Silvério, o meu professor, que me ensinou AdvPL, fiquei honrado pelo privilégio de ter um artigo dele aqui no Terminal de Informação.

Segue o LinkedIn do Emerson: https://www.linkedin.com/in/emerson-aparecido-silverio-9a205a18


Precisamos de semáforo quando uma rotina, ou trecho dela, precisa ser executado de forma serializada, ou seja, dois operadores ou instâncias não podem executá-la ao mesmo tempo.

Para agilizar a leitura, vou dividir o texto em duas partes.

Se você já conhece o conceito de semáforos, sinta-se à vontade para ir direto ao tópico Funcionamento.

Conceito de Semáforo

Imagine a criação de uma rotina que move os arquivos de um diretório para outro. Um algoritmo simples para esta rotina seria:

  • Ler os arquivos do diretório origem gravando em um vetor;
  • Fazer um loop neste vetor copiando os arquivos para o novo diretório;
  • Fazer novamente o loop no vetor excluindo os arquivos do diretório origem.

Obviamente, existem outras formas de se fazer este movimento de arquivos, mas vamos usar este exemplo para fins didáticos. Se dois operadores dispararem esta rotina ao mesmo tempo você pode ter resultados imprevisíveis, pois uma instância da sua rotina lerá todos os arquivos origem e inicia a cópia. Entretanto, se ao mesmo tempo a mesma rotina disparada pelo operador dois também leu a mesma quantidade de arquivos, mas quando ela vai copiar um arquivo, a rotina do operador um já copiou e pode acontecer um erro de “file not found”.

Para evitar casos como este e muitos outros é que precisamos do semáforo. Um semáforo faz com que um trecho do programa seja executado de forma serializada. No exemplo acima, a existência de um semáforo poderia ser checada, ativada ao iniciar a rotina que movimenta os arquivos e desativada após a execução. Desta forma o trecho de código entre a ativação do semáforo e a desativação ficaria protegida para que não fosse executada simultaneamente por mais que uma instância do programa. Bem, agora como programar isso?

Funcionamento do semáforo

Existem várias formas de se implementar um semáforo e isto pode ser feito em qualquer linguagem de programação.

1ª Forma: Utilizando um arquivo temporário

A forma mais simples é utilizar um arquivo temporário. Embora esta forma não seja indicada, tenho visto vários programas utilizarem esta técnica, por isso vou mostrá-la e depois vamos ver como melhorá-la. A ideia é gerar um arquivo de controle com qualquer nome e qualquer extensão, mas este nome precisa ser fixo no programa. Assim, ao entrar na rotina que precisa ser controlada por semáforo você confere se o arquivo existe (se existir é porque já há uma instância do seu programa executando o trecho serializado), cria o arquivo e ao final da rotina você exclui o arquivo, conforme mostrado abaixo:

                se arquivo-semaforo existe

                               mostra mensagem “Rotina já está sendo executada neste momento”

                senão

                               criar arquivo-semaforo

                               executar comando serializado

                               executar comando serializado

                               …

                               excluir arquivo-semaforo

                fimse

No algoritmo acima, a primeira instância do programa que cria o arquivo-semaforo impede que qualquer outra instância (ou operador) seja executada em paralelo.

Agora pense o seguinte: se durante a execução dos comandos (ou funções) serializadas ocorrer um erro e o sistema fechar, o que acontece com o seu arquivo de controle?

Exato! Ele vai ficar “preso” e a partir deste momento o sistema sempre vai acreditar que a rotina já está executando, pois não ocorreu a exclusão do arquivo-semaforo. É claro que o operador vai te ligar com este problema em uma sexta-feira à tarde, quando você estiver voltando dirigindo para casa e não se lembrar em que diretório você gravou o arquivo-semáforo. Pois neste caso, a única solução será a exclusão manual do arquivo-semaforo.

Mas não precisa se apavorar e nem desligar o celular às sextas à tarde. É possível melhorar este controle, mas ainda não é o mais indicado.

2ª Forma: Utilizando um arquivo temporário com distinção de operador

Vamos ver a segunda forma de controlar um semáforo: Além de criar o arquivo de controle, você pode colocar o nome do operador neste arquivo para que, ao conferir a existência, também confira quem criou este arquivo. Se for o próprio operador, o sistema pode continuar entrar no trecho serializado. Assim, se algum comando serializado travar o processo e fechar o sistema, quando o mesmo operador tentar novamente será possível a execução. Além disso você pode informar quem é o operador que está executando o processo. Veja o mesmo programa com o algoritmo melhorado:

                se arquivo-semaforo existe e operador diferente

                               mostra mensagem “Rotina já está sendo executada neste momento pelo operador <operador-arquivo-semaforo>”

                senão

                               criar arquivo-semaforo com nome do operador

                               executar comando serializado

                               executar comando serializado

                               …

                               excluir arquivo-semaforo

                fimse

Ok, esta solução melhora apenas um pouquinho. Imagine que um operador A execute a rotina e os comandos serializados demoram muito para terminar, então chega o horário do operador A ir embora e ele simplesmente desliga o computador. Então um operador B tenta executar a rotina e vai ver a mensagem na tela “Rotina já está sendo executada pelo operador A”! Bem, pelo menos nesse caso o operador B não vai ligar para você e sim para o operador A. De qualquer forma, ainda dá para usar uma outra abordagem em AdvPL.

3ª Forma: Funções GlbLock e GlbUnlock

Agora chegamos à terceira solução. Esqueça o uso de arquivos. Com esta técnica podemos fazer o controle de semáforos com funções específicas do AdvPL para isto. Eu, particularmente, utilizo estas funções para controlar semáforos em webServices, pois o TDN não deixa muito claro o comportamento das mesmas quando são utilizadas em load balance. Como o webService é utilizado quase sempre em um único appserver, o load balance deixa de ser uma preocupação.

Estou falando das funções GlbLock() e GlbUnlock(), que criam e liberam semáforos, respectivamente. Estas funções trabalham a nível de memória de instância. Sendo assim, se a rotina que criar o semáforo for interrompida por qualquer motivo, o semáforo é liberado mesmo que não passe pela desativação explícita.

Abaixo, segue um exemplo didático do uso destas funções para controlarem duas threads. Neste exemplo, a ideia é executar uma instância da rotina por 5 segundos e durante a execução outra instância deve aguardar o término.  Repare que a segunda thread é disparada um segundo após a primeira e ela aguarda até o final da execução da primeira thread para que comece seu processamento:

#include 'protheus.ch'
#include 'parmtype.ch'

/*/{Protheus.doc} TstLock 
Exemplo de serialização com controle de semaforo
@author Emerson Silverio
@since 13/05/2020
/*/
user function TstLock()
	// Inicia a primeira instância que será executada por 5 segundos
	ExecRot('1')
	
	// Aguarda 2 segundos para iniciar a segunda instância
	sleep(1000)

	// A segunda instância vai iniciar antes que a primeira tenha terminado
	ExecRot('2')
	
	FwLogMsgW('Programa principal finalizado!')
return

// Executa a funcao ExecInst SEM aguardar o termino da mesma
static function ExecRot(cRotNum)
	// Inicia a rotina u_execInst como uma nova thread para execução paralela
	StartJob("U_ExecInst", GetEnvServer(), .F.,;
		cRotNum; // parametro para u_execInst()
	)
return

#define _LOCK_TIMEOUT	10
/*/{Protheus.doc} ExecInst 
Rotina com trecho controlado por semaforo
/*/
user function ExecInst(cInstNum)
	local nInd := 1
	
	FwLogMsgW('Iniciada instância ' + cInstNum)
	// Ativar o semaforo
	while nInd < _LOCK_TIMEOUT .and. !GlbLock()
		FwLogMsgW('Instância ' + cInstNum + ' aguardando, tentativa ' + AllTrim(str(nInd)))
		sleep(500) // 500 milissegungos
		nInd++
	enddo
	FwLogMsgW('Semaforo ativado para instância ' + cInstNum)
	for nInd := 1 to 5
		FwLogMsgW('Instância ' + cInstNum + ' executando por ' + allTrim(str(nInd))+ '/5 segundos...')
		sleep(1000)
	next
	
	// Liberar o semaforo
	GlbUnlock()
	FwLogMsgW('Semaforo da instância ' + cInstNum + ' liberado!')
	FwLogMsgW('Finalizada instância ' + cInstNum)
return

// Wrapper para FwLogMsg
static function FwLogMsgW(cMens)
	FwLogMsg('INFO', , 'LOCK', FunName(), '', '01', cMens, 0, 0, {})
return

Resultado no console.log

[INFO ][SERVER] [13/05/2020 09:24:31] Starting Program U_TSTLOCK Thread 7844




Iniciada instância 1

Semáforo ativado para instância 1

Instância 1 executando por 1/5 segundos...

Instância 1 executando por 2/5 segundos...

Programa principal finalizado!




[INFO ][SERVER] [Thread 7844] [13/05/2020 09:24:33] Thread finished




Iniciada instância 2

Instância 2 aguardando, tentativa 1

Instância 2 aguardando, tentativa 2

Instância 1 executando por 3/5 segundos...

Instância 2 aguardando, tentativa 3

Instância 2 aguardando, tentativa 4

Instância 1 executando por 4/5 segundos...

Instância 2 aguardando, tentativa 5

Instância 2 aguardando, tentativa 6

Instância 1 executando por 5/5 segundos...

Instância 2 aguardando, tentativa 7

Instância 2 aguardando, tentativa 8

Semáforo da instância 1 liberado!

Finalizada instância 1

Semáforo ativado para instância 2

Instância 2 executando por 1/5 segundos...

Instância 2 executando por 2/5 segundos...

Instância 2 executando por 3/5 segundos...

Instância 2 executando por 4/5 segundos...

Instância 2 executando por 5/5 segundos...

Semáforo da instância 2 liberado!

Finalizada instância 2

 

Analisando a ordem das mensagens no log percebemos o momento em que o semáforo da instância 1 é liberado e logo abaixo a instância 2 inicia seu processamento.

Bem, por hoje é isto. Espero que tenha gostado e até a próxima.

Dan Atilio (Daniel Atilio)
Especialista em Engenharia de Software pela FIB. Entusiasta de soluções Open Source. E blogueiro nas horas vagas.

Deixe uma resposta