O lado negro do Application.ProcessMessages
Escrito por Carlos B. Feitoza Filho | |
Categoria: Artigos | |
Categoria Pai: Addicted 2 Delphi! | |
Acessos: 19600 |
Mais uma vez quero dizer que parte deste texto é uma tradução/versão do texto original disponível em http://delphi.about.com/od/objectpascalide/a/delphi-processmessages-dark-side.htm. Eu tomei a liberdade de incluir mais informações sobre mensagens do Windows e corrigir alguns pontos que ficaram nebulosos.
Você usa Application.ProcessMessages? Deveria reconsiderar!
Ao programar um manipulador de eventos no Delphi (como o evento OnClick de um TButton), chega o momento em que a sua aplicação precisa ficar ocupada por um tempo, por exemplo, o código precisa escrever um grande arquivo ou compactar alguns dados. Se você fizer isso você vai perceber que a sua aplicação parece ficar bloqueada. Seu formulário não pode mais ser movido e os botões não mostram sinais de vida. A aplicação parece ter travado.
A razão para isso acontecer é que uma aplicação Delphi tem apenas uma thread. O código que você está escrevendo representa apenas um monte de procedimentos que são executados pela thread principal da aplicação sempre que o evento ocorre. O resto do tempo esta thread principal está manipulando mensagens de sistema e outras coisas tais como funções de manipulação de forms e componentes. Então, se dentro de um manipulador de eventos você executar uma tarefa demorada, você vai terminar impedindo sua aplicação de manipular estas mensagens e uma solução comum para este tipo de problema é realizar chamadas a Application.ProcessMessages, doravante referido simplesmente como ProcessMessages.
O ProcessMessages manipula todas as mensagens que estão "aguardando" para serem manipuladas, tais como movimentação de janelas, cliques em botões, dentre outras. Ele é usado normalmente como uma solução simples que mantém sua aplicação rodando sem ficar bloqueada. Infelizmente o mecanismo por trás do ProcessMessages tem suas próprias características, as quais podem causar uma grande confusão!
Mas exatamente o que o ProcessMessages faz?
O Windows usa mensagens (Windows Messages) para "conversar" com todas as aplicações que estão em execução. As interações do usuário são enviadas ao formulário através mensagens, as quais são processadas no loop de mensagens da aplicação. Sim! Todo programa tem um loop infinito que fica ativo enquanto o programa estiver em execução e a cada iteração deste loop um item da fila de mensagens é processado. Por exemplo, se o botão esquerdo do mouse for pressionado enquanto o cursor se encontra em cima de um TButton, uma mensagem (WM_LBUTTONDOWN) é enviada ao TButton. Além da mensagem relacionada ao clique em si, existem outras que serão automaticamente geradas para complementar esta ação realizando tudo que precisa ser feito. Uma delas é a pintura (WM_PAINT) do TButton para um estado "pressionado" (baixo relevo) e, ao soltar o botão do mouse (ou sair da área-cliente deste botão), uma outra mensagem de pintura é enviada ao botão, de forma que ele seja repintado no estado "não pressionado" (alto relevo). Como o TButton pertence ao programa, estas mensagens são enviadas para a fila de mensagens da aplicação em questão e serão processadas em um momento qualquer dentro das iterações do loop de mensagens da aplicação.
Ao executar um procedimento demorado o loop de mensagens da aplicação para até que o procedimento demorado termine. Como o loop de mensagens está parado, a aplicação não processará qualquer mensagem da fila e é por isso que a aplicação parece ter travado, ela fica literalmente sem responder a nenhuma mensagem. Nesta situação o ProcessMessages faz exatamente o que seu nome diz, ele percorre TODAS as mensagens que estão na fila aguardando processamento, processa TODAS elas (executa seus manipuladores) e depois volta o controle para a thread principal, que continua o procedimento demorado.
Para tentar entender melhor, verifique o pseudocódigo abaixo:
while true do
begin
GetMessage(Msg, 0, 0, 0);
case Msg of
WM_LBUTTONDOWN: begin
for i := 0 to 99999999999 do
begin
ResolvaOsProblemasDoMundo(i);
CalcularPiComUmBilhaoDeCasasDecimais;
Application.ProcessMessages;
end;
end;
end;
end;
O loop mais externo (linha 1) seria o loop de mensagens. Note que a primeira coisa que ele faz (linha 3) é obter uma mensagem da fila de mensagens. Esta mensagem é então verificada por um seletor (linha 4) e o código do manipulador é executado (linhas 6 a 11). Se o código do manipulador for muito demorado o loop mais externo não vai completar sua iteração até que este código termine, logo, outras mensagens essenciais que seriam obtidas pelo GetMessage não serão obtidas e a aplicação fica travada.
Ao utilizar o ProcessMessages (linha 10), (muito) grosso modo, o que acontece é que o processo demorado (neste caso o loop interno) é suspenso, e todos os manipuladores de todas as mensagens da fila de mensagens serão executados, até que a fila se esvazie. Quando isso acontece o controle volta para o processo demorado e a linha de código subsequente à linha do ProcessMessages será executada.
À primeira vista isso não parece ser algo muito crítico, mas olhando mais de perto o que acontece, é possível notar que existe uma sequencia de acontecimentos e que por mais que você pense que várias coisas estão acontecendo ao mesmo tempo, na verdade elas ocorrem de forma ordenada, sequencial. Ao usar o ProcessMessages, esse ordem é quebrada. No momento em que o procedimento demorado é suspenso, mensagens que deveriam ser processadas apenas após seu término serão processadas imediatamente.
Por que eu devo evitar o ProcessMessages?
Pode ser que você nunca tenha problemas, mas certamente furar a fila de execução das mensagens pode causar uma grande confusão, pois executar ProcessMessages de qualquer maneira pode habilitar chamadas recursivas para qualquer manipulador de eventos novamente. Use o exemplo anexado a este artigo para entender melhor.
No trecho de código simplificado a seguir temos o manipulador de um evento OnClick. O loop FOR simula um processamento longo com chamadas ao ProcessMessages sendo executadas a cada iteração:
{in MyForm:}
WorkLevel: Integer;
{OnCreate:}
WorkLevel := 0;
procedure TForm1.WorkBtnClick(Sender: TObject);
var
cycle: Integer;
begin
inc(WorkLevel);
for cycle := 1 to 5 do
begin
Memo1.Lines.Add('- Work ' + IntToStr(WorkLevel) + ', Cycle ' + IntToStr(cycle);
Application.ProcessMessages;
sleep(1000) ; // ou alguma outra coisa demorada
end;
Memo1.Lines.Add('Work ' + IntToStr(WorkLevel) + ' ended.');
dec(WorkLevel);
end;
Sem ProcessMessages (linha 15) as seguintes linhas serão escritas no TMemo, se o botão for pressionado duas vezes em um curto intervalo de tempo:
- Work 1, Cycle 1
- Work 1, Cycle 2
- Work 1, Cycle 3
- Work 1, Cycle 4
- Work 1, Cycle 5
Work 1 ended.
- Work 1, Cycle 1
- Work 1, Cycle 2
- Work 1, Cycle 3
- Work 1, Cycle 4
- Work 1, Cycle 5
Work 1 ended.
Enquanto o processamento estiver em execução, o formulário não mostra qualquer reação, mas o segundo clique foi colocado na fila de mensagens pelo Windows, logo, imediatamente após o manipulador do evento OnClick terminar ele será chamado novamente.
Incluindo o ProcessMessages, a saída será muito diferente, veja:
- Work 1, Cycle 1
- Work 1, Cycle 2
- Work 1, Cycle 3
- Work 2, Cycle 1
- Work 2, Cycle 2
- Work 2, Cycle 3
- Work 2, Cycle 4
- Work 2, Cycle 5
Work 2 ended.
- Work 1, Cycle 4
- Work 1, Cycle 5
Work 1 ended.
Desta vez o formulário aparenta estar funcionando e aceita qualquer interação do usuário, então o botão é pressionando mais uma vez, mas agora, no meio do caminho durante o primeiro processamento. O ProcessMessages fará com que este segundo clique seja manipulado imediatamente. Todos os eventos de entrada serão manipulados, bem como quaisquer outras chamadas de função. Na teoria, durante cada chamada a ProcessMessages, qualquer quantidade de cliques e mensagens em geral serão manipuladas instantaneamente.
Além deste efeito de sobreposição de resultados, existe um efeito colateral mais evidente provocado indiretamente pelo ProcessMessages. No exemplo anexado a este artigo, após clicar no botão "Begin Work" com "Enable ProcessMessages" habilitado, mova a janela. Note que o processamento para completamente e só retorna quando você deixa de mover a janela. Isso acontece porque o ProcessMessages está tentando esvaziar a fila de mensagens, mas você, ao mover a janela, está introduzindo, a cada pixel movido, mais mensagens (WM_MOVE) à fila. Neste caso curioso, o ProcessMessages acaba se tornando o processamento demorado, porque dentro do loop, ao executá-lo, ele só vai devolver o controle ao mesmo, quando a fila de mensagens esvaziar, só que a fila nunca vai esvaziar enquanto mais mensagens de movimento estiverem entrando nela. Acho que já deu para entender que o ProcessMessages não serve para ser usado dessa forma, então, seja muito cuidadoso com seu código, ao usá-lo.
Vamos ver um exemplo diferente. Observe este simples pseudocódigo:
procedure OnClickFileWrite();
var
myfile := TFileStream;
begin
myfile := TFileStream.create('myOutput.txt');
try
while BytesReady > 0 do
begin
myfile.Write(DataBlock);
dec(BytesReady,sizeof(DataBlock));
DataBlock[2] := #13;
Application.ProcessMessages;
DataBlock[2] := #13;
end;
finally
myfile.free;
end;
end;
Este procedure escreve uma grande quantidade de dados e tenta manter a aplicação descongelada usando o ProcessMessages a cada vez que um bloco de dados é escrito.
Se o usuário clicar no botão que executa este procedure, enquanto ele já estiver em execução, o mesmo código será executado enquanto o arquivo ainda está sendo escrito. O arquivo não pode ser aberto uma segunda vez e consequentemente o procedure falha. Como resultado dessa falha, talvez haja alguma implementação para liberar buffers, então, DataBlock estaria vazio e uma possível terceira chamada ao procedure (ou uma chamada que já estivesse em execução anteriormente) iria subitamente levantar um Access Violation ao tentar acessá-lo. Neste caso, o código na linha 12 poderia funcionar, mas o código na linha 14 iria falhar. Mais uma vez se nota que a quebra na ordem dos acontecimentos (furar a fila de mensagens com o ProcessMessages) causa uma imensa bagunça e torna a lógica impossível de se entender.
Uma forma fácil de se contornar estes problemas seria bloquear o acesso aos botões do form configurando sua propriedade Enabled como false. Isso bloquearia qualquer interação com o usuário mas manteria os controles visivelmente ativos, o que não é uma boa ideia. Uma ideia melhor seria desabilitar cada um dos controles do formulário, menos aqueles que porventura precisem ficar ativos (um botão para cancelar o processamento, por exemplo), mas isso é mais complexo, pois seria necessário percorrer todos os controles e desabilitar por demanda cada um, além disso, ao terminar o processamento demorado, os controles precisariam ser percorridos novamente para serem habilitados, mas, digamos, você precisaria manter alguns deles desabilitados, caso estes já estivessem desabilitados ANTES do procedimento demorado. Resumindo, é muito trabalho pra permanecer usando algo que, na maioria dos contextos, não é correto.
Não tenha preguiça, use threads!
Desde o começo deste artigo eu apenas falei de algo demorado sendo feito ao clicar num botão, mas poderia ser um TMenuItem o um TAction. Não importa o que o procedimento demorado faça, na grande maioria das vezes ele é desencadeado por algo simples, como um clique. O OnClick é um evento do tipo TNotifyEvent e como o próprio nome da classe diz, ele deveria ser usado para procedimentos de curta duração. Para código pesado, a melhor forma é mover toda a codificação lenta para sua própria thread e manter no evento de curta duração apenas a criação e execução desta thread.
Levando em conta os problemas decorrentes do uso indiscriminado do ProcessMessages, bem como o trabalho adicional de ter que habilitar/desabilitar controles para manter o usuário na linha, o uso de uma segunda thread parece não ser assim tão complicado. Lembre-se de que mesmo poucas linhas de código podem travar uma aplicação por alguns segundos, por exemplo, abrir um arquivo do disco e ter que esperar pelo spin-up do disco terminar. Não seria muito legal se sua aplicação parecesse travada por conta de algum HD lento ou problemático não é mesmo?
Afinal o ProcessMessages é inútil?
Não, eu nunca disse isso! O que deve ser evitado é seu uso indiscriminado, mais especificamente, se você o utiliza dentro de um loop ou dentro de algo que é executado dentro de um loop, com certeza você está fazendo isto errado e seria melhor alterar seu código para usar uma thread separada. Regra geral, fica a dica: Se seus ProcessMessages estiverem em um loop, então está errado e deve ser evitado.