4 de setembro de 2009

Trabalhando com Threads em Delphi - Seções Críticas

Um dos aspectos mais importantes com que se preocupar quando desenvolvemos software envolvendo threads é o acesso a recursos compartilhados. Isto é, a forma com que vamos lidar com variáveis globais em memória, gravação e leitura de arquivos, impressão, acesso à interface visual, etc., de modo que seja garantido que todas as threads sempre encontrem esses recursos num estado apropriado para uso.

Pense, por exemplo, que você criou uma thread para imprimir relatórios em background para seu sistema. Você disponibiliza uma lista global onde o usuário pode ir adicionando novos relatórios a serem impressos. Sua thread de impressão busca nessa mesma lista qual é o próximo relatório a imprimir.

Antes de prosseguir, lembro alguns detalhes para tornar mais claro o contexto. Quando você escreve um programa em Delphi ou outra linguagem de alto nível, o compilador converte cada linha do seu programa em diversas instruções de baixo nível que podem ser executadas pelo computador. Toda thread é, então, composta por uma sequência das instruções originadas a partir das várias linhas do código que você escreveu. O Sistema Operacional executa as threads submetendo a sequência de instruções de cada uma delas por um tempo determinado (chamado time slice). Como esse tempo é determinado pelo Sistema Operacional, não há garantias de que todo o bloco gerado a partir da linha de código Delphi será executado todo de uma vez.
Operação de threads
Na figura acima, se o tempo destinado à Thread Principal se esgotar no ponto desenhado, o estado das variáveis de programa envolvidas pode não ter sido completamente atualizado. Outra Thread que acessar uma variável nessa condição encontrará lixo.

Voltando ao exemplo da thread de impressão, é preciso garantir que o código que manipula a lista seja executado como uma unidade. Sistemas Operacionais como o Windows disponibilizam um mecanismo denominado Seção Crítica para resolver esta questão. No Delphi e C++ Builder, a classe TCriticalSection (que está na unit syncobjs) encapsula o funcionamento desse mecanismo. A ideia é simples : cria-se uma instância de seção crítica para cada recurso que se queira proteger, proteção essa que se dá envolvendo o acesso ao recurso com a chamada a 2 funções - uma para entrar na seção crítica e a outra para liberar o acesso ao recurso novamente. Ao adicionar um novo Job à lista de impressão:
try
_scPrintJobs.Enter;
_Jobs.Add (pNovoJob);
finally
_scPrintJobs.Leave;
end;
E também quando a Thread responsável pela impressão for remover um job para executá-lo:
try
_scPrintJobs.Enter;
lJob := _Jobs.Items[pJobIdx];
_Jobs.Delete (pJobIdx);
finally
_scPrintJobs.Leave;
end;
É conveniente utilizar tratamento de exceções (par try/finally) pois a seção crítica é um recurso bastante sensível. Se ocorrer algum problema e o método Leave nunca for executado, o seu programa fatalmente vai deixar de funcionar. É bom usar com critério o quê vai ser colocado dentro da seção crítica pois um processamento pesado pode fazer com que o restante da aplicação pare de responder.

Por questão de organização, a criação da seção crítica pode ser feita na própria unit reservada ao recurso que ela deve proteger:
var _scPrintJobs : TCriticalSection;
{...}
initialization
_scPrintJobs := TCriticalSection.Create;

finalization
_scPrintJobs.Destroy;
end.
Seções Críticas devem ser utilizadas somente dentro de um mesma aplicação. Se pretende construir uma biblioteca (DLL) que poderá ser utilizada por mais de uma aplicação ao mesmo tempo, o mecanismo mais apropriado é o Mutex (acesso mutuamente exclusivo). Os Mutexes funcionam do mesmo modo que as seções críticas e, embora possam ser usados também quando o escopo é o mesmo processo, são ligeiramente mais lentos.

2 comentários :

Anônimo disse...

Olá,
tenho um recurso de impressão que é compartilhado(metodo de impressão global) por um conjunto de threads e tbm pelo usuário ao solicitar uma impressão de relatório por exemplo.
Ao criar uma sessão critica para tratar este caso, estou tendo problema pq o semafaro parece estar sempre aberto para todas as requisições.
Existe alguma diferença entre semafaro quando pe acessado por formulário e thread?

obrigado.

Luís Gustavo Fabbro disse...

Não há diferença entre o uso de semáforos na thread principal e em outras threads criadas manualmente.

Como é feita a sincronização em seu caso? Pode haver alguma falha na aplicação do recurso; liberação antes da hora, por exemplo.

[]s

Postar um comentário

OBS: Os comentários enviados a este Blog são submetidos a moderação. Por isso, eles serão publicados somente após aprovação.

Observação: somente um membro deste blog pode postar um comentário.