Social Icons

domingo, 23 de abril de 2017

O Lado Negro do "Application.ProcessMessages"

Aviso!
Este artigo é uma tradução do artigo: The Dark Side of Application.ProcessMessages in Delphi Applications.


Quando um evento é tratado no Delphi (como o evento OnClick de um botão), algumas vezes sua aplicação fica ocupada, por exemplo quando necessita escrever um arquivo grande ou realizar a compressão de dados.

Se você fizer isso, perceberá que sua aplicação vai ficar como se estivesse bloqueada. Seu formulário não poderá ser movido e os botões não mostrarão sinal de vida.

Da a impressão que a aplicação está travada.

A motivo é que uma aplicação Delphi é thread única (single thread). O código que você escreve representa um monte de procedimentos que são chamados pela thread principal do delphi sempre que um evento ocorre. O restante do tempo a thread principal está tratando mensagens do sistema e outras coisas como formulários e funções de manipulação de componentes. 

Então, se você não finalizar a processamento demorado que seu evento está fazendo, você irá impedir que a aplicação trate as mensagens.

Uma solução comum para este tipo de problema é chamar "Application.ProcessMessages". "Application" é um objeto global da classe TApplication.

O Application.ProcessMessages processa todas as mensagens que estão em espera, tais como os movimentos da janela, cliques de botões e assim por diante. É comumente usado como uma solução simples para manter sua aplicação em "funcionamento". 

Infelizmente o mecanismo por traz do "ProcessMessages" tem suas próprias características, as quais podem causar grande confusão!

O QUE O PROCESSMESSAGES FAZ?

"ProcessMessages" processa todas as mensagens do sistema que estão aguardando na fila de mensagens da aplicação. O windows usa mensagens para conversar com todas as aplicações que estão em execução. A interação do usuário com o formulário é realizada através de mensagens e o "ProcessMessages" processa todas elas. Se o mouse está apertando um botão, por exemplo, o ProcessMessages faz tudo que poderia acontecer nesse evento como repintar o botão com o estado de "pressionado" e, claro, a chamada do processamento do OnClick() caso você o associou.

Este é o problema: qualquer chamada ao ProcessMessages pode conter uma chamada recursiva a qualquer evento novamente. Aqui está um exemplo:

Use o seguinte código para o evento OnClick. A instrução "for" simula um processamento longo e faz uma chamada ao ProcessMessages de vez em quando.

Este foi simplificado para melhor legibilidade:

{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) ; // or some other work
   end;
   Memo1.Lines.Add('Work ' + IntToStr(WorkLevel) + ' ended.') ;
   dec(WorkLevel) ;
 end;

SEM "ProcessesMessages" as seguintes linhas são escritas no memo, se o botão for pressionado DUAS VEZES em um curto espaço 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 procedimento está ocupado, o formulário não mostra nenhuma reação, mas o segundo click foi colocado na fila de mensagens windows.

Logo após o "OnClick" terminar ele será chamado novamente.
INCLUINDO "ProcessMessages", a saída pode ser muito diferente:

 - 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 parecerá estar em funcionando novamente e aceita qualquer interação do usuário. Então o botão é pressionado novamente na metadade do seu primeiro processamento,o qual será tratado imediatamente. Todos os eventos de entrada serão tratados como qualquer outra chamada de função.

Em teoria, durante toda chamada ao "ProcessMessages" QUALQUER quantidade de cliques e mensagens do usuário podem acontecer "No mesmo instante"

Então tome cuidado em seu código!

Um outro exemplo (em um simples pseudo-có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; {test line 1}
      Application.ProcessMessages; 
      DataBlock[2] := #13; {test line 2}
    end;
  finally
    myfile.free;
  end;
end;


Esta função escreve uma quande quantidade de dados e tenta "desbloquear" a aplicação usando "ProcessMessages" a cada vez que um bloco de dados é escrito. Se o usuário clica novamente no botão, o mesmo código será executado enquanto o arquivo ainda é escrito. Portanto, o arquivo não pode ser aberto uma segunda vez e o procedimento falha.

Talvez sua aplicação faça uma recuperação de erro como liberar os buffers.

Como um possível resultado o "bloco de dados" será liberado e o primeiro código causará "repentimanente" um "Access Violation" quando tentar acessa-lo. Nesse caso: o teste na linha 1 funcionará, o teste na linha 2 falhará.

A melhor maneira:

De uma maneira fácil, você poderia setar o formulário "enabled := false", que bloqueia todos as entradas do usuário, mas NÃO mostrará isso ao usuário (todos os botões não ficarão acinzentados).

Uma melhor forma seria setar todos os botões para "desabilitado", mas isto pode ser complexo se você quer manter um botão "Cancelar" por exemplo. Você também precisaria percorrer todos os componentes para desabilitá-los e quando eles são ativados novamente, você precisa checar se existe algum que esteja desabilitado.

Você pode desabilitar os controles filho de um contêiner quando a propriedade Enabled for alterada.

Como o nome de classe "TNotifyEvent" sugere, ele só deve ser usado para reações de curto prazo para o evento. Para o código demorado, a melhor maneira em minha honesta opinião, é colocar todo o código "lento" em uma Thread própria.

Quanto aos problemas com "ProcessMessages" e / ou a ativação e desativação de componentes, o uso de uma segunda thread parece não ser muito complicado em tudo.

Lembre-se de que mesmo as linhas simples e rápidas de código podem ficar paralisadas por segundos, por exemplo: abrir um arquivo em uma unidade de disco pode ter que esperar até que a rotação da unidade tenha terminado. Não parece muito bom se o aplicativo parece falhar porque a unidade é muito lenta.

É isso aí. Da próxima vez que você adicionar "Application.ProcessMessages", pense duas vezes ;)

Nenhum comentário:

Postar um comentário

Diga-nos, o que achou deste artigo?