18 de agosto de 2009

Trabalhando com Threads em Delphi - exemplo básico

Neste post, vou dar sequência ao post anterior sobre Threads e mostrar um exemplo bastante básico de como criar uma aplicação usando Thread em Delphi.

A ideia da aplicação é permitir que o usuário inicie um processo demorado mas ainda possa continuar interagindo com o programa, aí incluindo opção para cancelar a execução. Para isso, vou montar um Form com um botão para iniciar o processo, um outro botão para cancelar sua execução e uma barra de progresso para mostrar a evolução da execução:
Form para testar Thread
Completa a tela um temporizador (Timer1) que será ativado junto com a thread e terá a responsabilidade de monitorar o progresso da execução.

O primeiro passo será criar uma classe para representar a Thread. Em Delphi, acesse o menu File -> New -> Other -> Thread Object para criar um esqueleto vazio para a Thread; este caminho pode diferir um pouco, dependendo da versão do Delphi. O esqueleto gerado será basicamente o reproduzido abaixo:
TWMinhaThread = class(TThread)
protected
procedure Execute; override;
end;

O código gerado não sobrescreve o construtor padrão. Como teremos uma interação com o Form principal da aplicação, será preciso montar um construtor especial que aceite o nosso Form como parâmetro:
constructor TWMinhaThread.Create(CreateSuspended: Boolean; AForm: TForm);
begin
inherited Create (CreateSuspended);
_Form := AForm;
FreeOnTerminate := false;
end;

A primeira linha do construtor chama o construtor Create da classe base (inherited) passando-lhe o parâmetro para indicar se a thread deve iniciar imediatamente ou permanecer suspensa. FreeOnTerminate é ajustado como false de modo que nós teremos que destruir instâncias da classe manualmente.

A procedure Execute é onde deve ficar o código executado pela thread. Para simular um processo demorado, incluí nesta procedure um laço para contar até 100, aguardando alguns milissegundos em cada passo para nos dar a oportunidade de poder cancelar a execução.
procedure TWMinhaThread.Execute;
begin
_Posicao := 0;
while (not Terminated)
and (_Posicao < 100) do
begin
Inc (_Posicao);
Synchronize (AtualizaTela);
Sleep (10);
end;
end;

Chamo atenção para a linha Synchronize (AtualizaTela);. A função AtualizaTela faz a atualização da barra de progresso no Form, ajustando a propriedade Position com o valor atual de _Posicao:
procedure TWMinhaThread.AtualizaTela;
var lForm1 : TForm1;
begin
lForm1 := _Form As TForm1;
lForm1.pbProgresso.Position := _Posicao;
end;

O problema é que objetos da VCL não podem ser diretamente atualizados numa Thread que não seja a principal pois a VCL não é thread-safe. Assim, o método Synchronize tem que ser chamado para fazer o serviço. É passado a ele uma procedure sem parâmetros - no caso, o AtualizaTela - e a execução dessa procedure é sincronizada com a execução da Thread principal, garantindo a segurança da atualização no ambiente multi-threaded.

No Form, respondo ao clique do botão Iniciar criando uma instância da Thread para execução imediata e ligando o Timer que checa o status da execução dela:
_MinhaThread := TWMinhaThread.Create (false, Self);
Timer1.Enabled := true;

No evento do Timer, verificamos se a Thread chegou ao fim da execução para fazer a limpeza e avisar o usuário:
if (_MinhaThread._Posicao >= 100) then begin
Timer1.Enabled := false;
_MinhaThread.Free;
_MinhaThread := Nil;
end;

Não está representado neste trecho mas deve ser testado também se o usuário não pressionou o botão Cancelar, cuja resposta chama o método Terminate da Thread, ajustando sua propriedade Terminated. Veja que o laço while existente no método Execute da thread testa também o valor do Terminated para saber se o usuário solicitou o encerramento. Em caso afirmativo, a thread é interrompida imediatamente, mesmo que o processamento implementado nela ainda não tenha chegado ao fim.

Certamente há outras formas mais elaboradas de se monitorar o progresso da execução de uma Thread numa aplicação gráfica Windows. A que prefiro envolve a criação de uma mensagem no Windows, de modo que a própria Thread vai notificando sua evolução. Há outros posts aqui no blog mostrando também outras técnicas de sincronização (aqui e aqui).

Para fazer o download do projeto com o exemplo, clique aqui. Este exemplo foi construído com Delphi 2005 mas pode ser compilado em outras versões com poucas alterações.

16 comentários :

Anônimo disse...

Rpz!!! Simplesmente Fantástico!!! Parabéns, Saúde, Paz e Sucesso pra vc!!!

Anônimo disse...

Muito Bom mesmo

Anônimo disse...

Realmente um excelente artigo!!!

Anônimo disse...

Olá tenho uma duvida no delphi com Threads , gostaria de desenvolver um sistema basico aonde o cliente digita um memo palavra chaves , por exemplo e cadastrado o google no aplicativo para fazer essas busca no caso ira coletar todos o email com essa plavra chave , no caso queria saber como fazer isso por exemplo o google achou 900 paginas o problema tb e como acessa 1 link x so que a ideia e usar Threads para trazer o resultado mais rapido possivel alguem sabe como fazer isso? meu email é ntnetx@hotmail.com

Luís Gustavo Fabbro disse...

Vc pode usar o protocolo HTTP para acessar cada link em uma thread separada. Veja o post Trabalhando com HTTP em Delphi pra ver como usar o componente de HTTP.

[]s

Thiago Diniz disse...

Parabéns pelo post. Muito explicativo.

Anônimo disse...

Excelente post sobre Thread, aprendi muita coisa! Obrigado!
Att.Rodolfo Nogueira

Anônimo disse...

se me congela la ventana ...de mi aplicación....inclusive usando hilos...

Luís Gustavo Fabbro disse...

Que tipo de processamento você está fazendo na thread extra? Atualizações em componentes visuais da VCL são sempre feitas na thread principal; portanto, processos longos de atualização feitos a partir de outras threads podem dar a impressão de congelar a tela.

[]s

Rogério Otero disse...

Olá! Tenho uma dúvida em relação as Threads. Gostaria de saber como se deixa uma thread oculta, fazendo com que um programa não consiga lê-la e assim não "hookando" a .dll que criei.

Luís Gustavo Fabbro disse...

Rogério

Nunca precisei fazer isso mas creio que "esconder" a thread não é possível. Ao que parece, o grupo de funções Thread32First (exemplo aqui) sempre conseguirá detectar todas as threads do sistema. No entanto, talvez você consiga seu intento criando a thread manualmente com um descritor de segurança. Veja a documentação do CreateThread e do SECURITY_DESCRIPTOR.

[]s

Unknown disse...

Quebrou o galho pra min, valeu.
Tava com problema na hora de sincronizar :P
Valeu, brigadão!

Unknown disse...

Quero aprender sobre Thread sou iniciante nisso, ao montar o construtor (seguindo exatamente o tutorial) a parte _Form := AForm; para começar oque é _Form e na minha aplicação fica assinalado como fica uma variável sem Declaração, alguém me explica como resolver por favor, Obrigado.

Luís Gustavo Fabbro disse...

Ale

Embora eu não tenha representado no post, _Form deve ser declarado como um membro da classe de thread. Assim, a thread poderá acessar as propriedades e funções do form de sua aplicação:

TWMinhaThread = class(TThread)
protected
_Form: TForm;
procedure Execute; override;
end;

[]s

Unknown disse...

Olá, gostei muito do seu post, porém não consegui entender a parte do AtualizaTela, como faço para passar o componente do form para a thread?

Luís Gustavo Fabbro disse...

Rogerio

Uma referência ao Form é mantida pela thread. Ao chamar o construtor da thread, o Form é passado como parâmetro, como mostra a seguinte linha de código extraída do post:

_MinhaThread := TWMinhaThread.Create (false, Self);

OBS:Self é o próprio Form.

[]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.