Software">
Threads No Delphi
Threads No Delphi
Threads No Delphi
– Parte I
Publicado em 31/01/2013 por Diego Garcia
Thread é um termo conhecido por qualquer “informata” que se preze, mas para quem não se recorda,
qualquer aplicação utiliza no minimo uma thread (a thread principal ou main thread) onde o fluxo do
processamento é executado, nessas aplicações os comandos são executados um por vez de forma
sequencial. Até ai nada demais, mas a coisa passa a ficar interessante quando usamos mais de uma
thread em uma aplicação, fazendo com que seja possível executar processamentos de forma paralela.
No dia a dia usamos as threads para gerenciar tarefas que precisam ser executadas paralelamente,
melhorar desempenho de processamentos, etc. Mas não se iluda, usar threads é simples, mas requer
alguns cuidados.
Deixando a teoria e o blablabla de lado, vamos ver um pouco sobre como funcionam as threads no
Delphi.
Começaremos criando um exemplo bem básico, faremos um método que escreve de 1 a N linhas em um
arquivo de texto tendo o intervalo de 1 milissegundo a cada linha escrita, o nome deste arquivo sempre
será hora, minuto, segundo e milésimo de criação e o numero de linhas escritas, ex: 14-30-01-
200_total_linhas_1500.txt. Inicialmente criaremos esse método da forma comum e depois passaremos
para uma thread, assim conseguiremos realizar algumas comparações no resultado final. Para a
execução comum, faremos dessa forma:
Para testar esse método, faremos com que ele seja executado 3 vezes e analisaremos a diferença de
tempo de gravação entre um e outro, lembrando que, como ainda não estamos trabalhando com
threads, será escrito um arquivo por vez.
1 escreverArquivoDeTexto(ExtractFileDir(ParamStr(0)),10500);
2 escreverArquivoDeTexto(ExtractFileDir(ParamStr(0)),10200);
3 escreverArquivoDeTexto(ExtractFileDir(ParamStr(0)),10400);
Usei um número grande de linhas para que fosse possível medir o desempenho. Ao executar a
sequência acima, tive os seguintes arquivos criados no diretório do executável de teste:
1 14-47-22-711_total_linhas_10500.txt
2 14-47-33-078_total_linhas_10200.txt
3 14-47-43-661_total_linhas_10400.txt
Sendo assim, podemos concluir que levou cerca de 10 segundos de diferença para cada arquivo.
Faremos agora novamente essa rotina, porém, faremos com que ela seja executada dentro de uma
thread.
Para facilitar nossas vidas, o Delphi já possui o esqueleto padrão para a criação de uma thread, basta ir
em FILE -> NEW -> OTHER -> THREAD OBJECT, após selecionar Thread Object será solicitado o nome da
classe da sua Thread (para nosso exemplo utilizei TArquivoTextoInThread) e pronto, teremos o seguinte
código já montado em uma nova unit:
1 type
2 TArquivoTextoInThread = class(TThread)
3 private
4 { Private declarations }
5 protected
6 procedure Execute; override;
7 end;
REPORT THIS AD
Antes de voltarmos a falar sobre conceitos, vamos primeiro preparar a nossa classe para que ela escreva
em um arquivo de texto. Sendo rápido e prático, criaremos dois atributos privados na classe, um
referente ao número de linhas a serem escritas e outro referente ao diretório onde o arquivo será
criado. Feito isso, passaremos a nossa rotina de criar o arquivo de texto para dentro desta classe,
ficando desta forma:
1 type
2 TArquivoTextoInThread = class(TThread)
3 private
4 FLinhasAImprimir : integer;
5 FDirArquivo : String;
6 procedure escreverArquivoDeTexto();
7 function gerarNomeArquivo():string;
8 protected
9 procedure Execute; override;
10 public
constructor Create (const CreateSuspended : boolean; const linhasAImprimir :
11
integer; const dirArquivo : string);
12 end;
Certo, eu fiz algumas coisas a mais, mas vamos por partes, primeiro, vamos ver como ficou nosso
método escreverArquivoDeTexto().
1 procedure TArquivoTextoInThread.escreverArquivoDeTexto;
2 var
3 linhasArquivoTexto : TStringList;
4 I: Integer;
5 begin
6 linhasArquivoTexto := TStringList.Create;
7 try
8 for I := 1 to Self.FLinhasAImprimir do
9 begin
10 linhasArquivoTexto.Add(format('Linha numero %d',[i]));
11 Sleep(1);
12 end;
13 linhasArquivoTexto.SaveToFile(Self.gerarNomeArquivo());
14 finally
15 linhasArquivoTexto.Free;
16 end;
17 end;
REPORT THIS AD
Basicamente nada mudou, só para melhorar a legibilidade do código foi criado um método para gerar o
nome do arquivo de texto, porém ele tem a mesma lógica utilizada anteriormente:
Sendo assim, até aqui nenhuma novidade, mas agora vamos para um pouco de conceitos analisando o
construtor que fiz para a classe:
A classe TThread possui uma propriedade booleana chamada FreeOnTerminate que quando alterada
para true a thread é liberada automaticamente da memória após a sua execução. Outra coisa que é
importante de notar é o parâmetro CreateSuspended que o construtor da classe TThread recebe, ele
basicamente define se a thread será criada suspensa ou não, ou seja, se a thread será executada
automaticamente após a instância ser criada (CreateSuspended = false) ou se será necessário iniciar a
execução da thread de forma manual (CreateSuspended = true) através do método Start. Para completar
a implementação da nossa classe, vamos ver como ficou o método Execute() da thread:
1 procedure TArquivoTextoInThread.Execute;
2 begin
3 escreverArquivoDeTexto();
4 end;
REPORT THIS AD
Ok, imagino que você esperava mais do que simplesmente uma chamada para o
método escreverArquivoDeTexto() porém, o segredo todo está ai, o método Execute() é exatamente o
método que será executado assim que a thread for iniciada. Com isso, a nossa thread está pronta. Agora
vamos fazer o mesmo teste que fizemos anteriormente mas dessa vez utilizando nossa thread:
1 var
2 oArquivoTextoInThread1 : TArquivoTextoInThread;
3 oArquivoTextoInThread2 : TArquivoTextoInThread;
4 oArquivoTextoInThread3 : TArquivoTextoInThread;
5 begin
oArquivoTextoInThread1 :=
6
TArquivoTextoInThread.Create(false,10500,ExtractFilePath(ParamStr(0)));
oArquivoTextoInThread2 :=
7
TArquivoTextoInThread.Create(false,10200,ExtractFilePath(ParamStr(0)));
oArquivoTextoInThread3 :=
8
TArquivoTextoInThread.Create(false,10400,ExtractFilePath(ParamStr(0)));
9 end;
Executando a rotina acima, como determinei que minha thread não irá iniciar suspensa, tive os
seguintes arquivos criados no diretório do executável:
1 15-42-46-946_total_linhas_10200
2 15-42-47-167_total_linhas_10400
3 15-42-47-256_total_linhas_10500
Note que o último arquivo a ser criado foi o de 10500 linhas mesmo sendo o primeiro a ter sua rotina
executada e outra coisa muito importante é a diferença de tempo na criação dos arquivos, se antes sem
thread tivemos uma demora de 10 segundos para cada arquivo, aqui não passou de poucos milésimos,
tivemos esse resultado pois cada arquivo demora cerca de 10 segundos para ser gerado, porém, como
os 3 estavam sendo gerados ao mesmo tempo de forma paralela, não foi necessário utilizar uma fila de
processamento.
Como o intuito deste post foi só demonstrar o uso de threads, não entrei em mais detalhes, porém, na
próxima parte veremos um pouco mais sobre como controlar o ciclo de vida de uma thread.
Dando continuidade à esse estudo introdutório, veremos mais sobre como manipular uma thread. Na
primeira parte vimos como criar uma thread e a importância do método Execute(). Vejamos agora
outros métodos e propriedades da classe TThread:
Start(): Basicamente inicia a execução de uma thread, este método é necessário quando
criamos uma thread definindo o seu estado inicial como Suspended (suspensa), por
exemplo:
Terminate(): Utilizado para parar a execução de uma thread, basicamente este método seta
a variável Terminated da thread para true, assim sendo, é necessário realizar o controle
desse status na rotina executada na thread, por exemplo:
Suspend(): Este método pausa a execução da thread suspendendo a sua execução. Mas
atenção!!! Este método se tornou obsoleto (deprecated) a partir do Delphi XE:
1 oMinhaThread.Suspend();
1 oMinhaThread := TMinhaThread.Create(true);
2 oMinhaThread.OnTerminate := reportarFimThread;
REPORT THIS AD
Então vamos lá, ver um pouco mais sobre as Threads no Delphi. Nessa terceira parte, veremos como
criar blocos protegidos em uma thread para, entre outras coisas, interagir com a GUI (Graphical User
Interface) com uma maior segurança. Com o intuito de simplificar o estudo, faremos o já clássico
exemplo das barras de progressão sendo manipuladas paralelamente.
Basicamente nosso exemplo será uma thread que receberá o ponteiro de um TProgressBar. Essa thread
irá setar a posição do TProgressBar em zero e depois fará um laço onde o TProgressBar será preenchido
até a sua posição final (propriedade max). Seguindo a linha de raciocínio da primeira parte do estudo,
vamos criar uma nova thread com a seguinte estrutura:
1 type
2 TProcInThread = class(TThread)
3 private
4 FPgbProgresso : TProgressBar;
5 procedure atualizarProgressBar;
6 procedure avancarProgressBar;
7 public
8 constructor Create(CreateSuspended : boolean; pgbProgresso : TProgressBar);
9 protected
10 procedure Execute; override;
11 end;
Seguindo a mesma lógica dos posts anteriores, nosso construtor não apresenta nenhuma novidade:
constructor TProcInThread.Create(CreateSuspended : boolean;pgbProgresso:
1
TProgressBar);
2 begin
3 Self.FPgbProgresso := pgbProgresso;
4 inherited Create(CreateSuspended);
5 end;
1 procedure TProcInThread.avancarProgressBar;
2 begin
3 Self.FPgbProgresso.StepBy(1);
4 end;
1 procedure TProcInThread.Execute;
2 begin
3 atualizarProgressBar();
4 end;
Agora sim, vamos ver o método mais importante da nossa thread, o atualizarProgressBar():
1 procedure TProcInThread.atualizarProgressBar;
2 var
3 I: Integer;
4 begin
5 Self.FPgbProgresso.Position := 0;
6 for I := 0 to Self.FPgbProgresso.Max - 1 do
7 begin
8 if Self.Terminated then
9 abort();
10 Synchronize(Self.avancarProgressBar);
11 Sleep(100);
12 end;
13 end;
Coloquei o Sleep(100) para que fosse possível visualizar melhor a execução, fora isso, chegamos
finalmente no X da questão, o método Synchronize(). Este método executa a chamada de um
determinado método (passado como parâmetro) dentro da thread principal da aplicação (main thread),
ou seja, seguindo o nosso exemplo, o comando:
1 Synchronize(Self.avancarProgressBar);
Faz com que o método avancarProgressBar() seja executado dentro da thread principal da aplicação,
fazendo com que o procedimento do método seja protegido e não entre em conflito com outras threads
que manipulem os mesmos dados que a thread atual.
Com isso, nossa thread já está pronta. Para vê-la em execução, vamos criar uma formulário com 3
TProgressBar e 2 TButtons. No private do formulário, crie 3 objetos do tipo TProcInThread que é a classe
da nossa thread:
1 type
2 TForm1 = class(TForm)
3 ProgressBar1: TProgressBar;
4 ProgressBar2: TProgressBar;
5 ProgressBar3: TProgressBar;
6 Button1: TButton;
7 Button2: TButton;
8 private
9 oProcInThread1 : TProcInThread;
10 oProcInThread2 : TProcInThread;
11 oProcInThread3 : TProcInThread;
12 public
13 end;
Note que a terceira thread (oProcInThread3) não foi criada como Suspended, sendo assim não é
necessário chamar o método Start() para iniciar a execução da thread e a segunda thread
(oProcInTread2) foi configurada para ser liberada da memória assim que termine sua execução.
Agora em nosso Button2, liberaremos as threads da memória, mas somente as que não são liberadas
automaticamente:
1 Synchronize(Self.avancarProgressBar);
1 Synchronize(
2 procedure ()
3 begin
4 Self.FPgbProgresso.StepBy(1);
5 end
6 );
Realmente, a primeira impressão não é a das melhores, mas ainda sim pode ser algo muito útil. Vale a
pena se aprofundar um pouco mais nos estudos sobre Anonymous Method principalmente no que diz
respeito ao seu acesso e escopo de variáveis em comparação aos ponteiros para métodos, porém, esse
não é o foco deste post, na verdade, mostrei a existência de métodos anônimos para poder falar sobre
as Threads Anônimas ou Anonymous Threads.
Imagine um cenário em que você precise realizar um processamento relativamente pesado, porém não
faz sentido bloquear o sistema para o usuário até que esse processamento seja concluído, já que
teoricamente esse processamento não é de interesse do usuário, como por exemplo, limpar todos os
arquivos de log mais antigos de um sistema. Qual seria sua primeira sugestão? Tenho certeza que seria
“criar uma thread”. Mas pense no trabalho de criar uma thread (criar uma classe descendente de
TThread, implementar o método execute, etc.). Se o propósito da thread for muito simples, podemos
criar uma thread anônima. Mas como fazemos isso?
1 procedure TfrmPrincipal.apagarLogAntigoComThread;
2 begin
3 TThread.CreateAnonymousThread(
4 procedure()
5 var
6 SearchRec : TSearchRec;
7 i : integer;
8 sDir : string;
9 begin
1
sDir := ExtractFileDir(ParamStr(0));
0
1
i := FindFirst(sDir + '\*.log',0,SearchRec);
1
1
while i = 0 do
2
1
begin
3
1 if StrToInt(Copy(SearchRec.Name,1,4)) < (strtoint(FormatDateTime('yyyy',Now))
4 - 1) then
1
deleteFile(sDir + '\' +SearchRec.Name);
5
1
i := FindNext(searchRec);
6
1
end;
7
1
end
8
1
).Start;
9
2
end;
0
Inicialmente é um pouco complicado para ler o código, por isso caprichei na indentação. Note que não
foi necessário atribuir a thread anônima a um objeto e repare que já estou iniciando a sua execução
invocando o método Start(). Como disse anteriormente, essa thread será executada e liberada da
memória automaticamente.
A nossa thread basicamente procura na pasta do executável da aplicação, todos os arquivos com a
extensão .log e depois (levando em consideração que o padrão do nome dos arquivos de log seja algo
como ‘AAAA-MM-DD.log’ por exemplo), deleta todos que forem do ano retrasado para trás, por
exemplo, se estamos em 2013, todos arquivos que começarem com 2011, 2010, 2009…. serão
deletados. Imaginando que essa thread seria executada na inicialização de um
sistema, poderíamos fazer desta forma por exemplo:
1 procedure TfrmPrincipal.inicializarAplicacao;
2 begin
3 apagarLogAntigoComThread();
4 criarArquivosTemporarios();
5 if conectarAoBancoDeDados() then
6 .
7 .
8 .
9 end;
Mesmo que tivesse mais de 365 arquivos de log antigos, o processamento seguiria paralelo às outras
rotinas de inicialização, tornando o processo como um todo mais rápido. Caso fosse necessário atualizar
um componente visual de dentro da thread anônima, poderíamos usar o synchronize, mas como
estamos lidando com uma thread anônima, a forma de chamar o synchronize é um pouco diferente.
Vamos imaginar que nossa thread de limpar log, após varrer o diretório, escreva em um statusBar
indicando que o processamento foi encerrado, teríamos algo assim:
1 procedure TfrmPrincipal.ApagarLogAntigoComThread;
2 begin
3 TThread.CreateAnonymousThread(
4 procedure()
5 var
6 SearchRec : TSearchRec;
7 i : integer;
8 sDir : string;
9 begin
1
sDir := ExtractFileDir(ParamStr(0));
0
1
i := FindFirst(sDir + '\*.log',0,SearchRec);
1
1
while i = 0 do
2
1
begin
3
1 if StrToInt(Copy(SearchRec.Name,1,4)) < (strtoint(FormatDateTime('yyyy',Now))
4 - 1) then
1
deleteFile(sDir + '\' +SearchRec.Name);
5
1
i := FindNext(searchRec);
6
1
end;
7
1
//atualizar componente vcl
8
1
TThread.Synchronize(TThread.CurrentThread,
9
2
procedure
0
2
begin
1
2 frmPrincipal.stbRodape.SimpleText := 'Arquivos de log antigos deletados com
2 sucesso';
2
end
3
2
);
4
2
end
5
2
).Start;
6
2
end;
7
Veja que agora é necessário informar qual thread está invocando o synchronize. Como não atribuímos
explicitamente a instância da thread anônima para um objeto, podemos usar o
método TThread.CurrentThread(). Caso tivéssemos atribuído a thread para um objeto, bastaria passar
esse objeto para o método sychronize:
1 TThread.Synchronize(oThreadAnonima,frmPrincipal.atualizarStatusBar('Log processado'));
Eu particularmente não recomendo de forma alguma o uso de threads anônimas para rotinas
complexas, mas, sem dúvidas que as Anonymous Thread facilitam nossas vidas, já que podemos realizar
processamentos pesados porém simples de forma multi-thread, sem a necessidade de criar uma classe
especifica de thread para isso.
Na próxima parte, veremos algumas dicas para realizar o debug de nossas threads. Até lá.
Chegamos ao final desse estudo introdutório sobre o fantástico mundo do processamento concorrente,
proporcionado pelo uso das threads. Nessa quinta e última parte, veremos algumas dicas sobre como
não enlouquecer ao realizar debugging em um ambiente multi thread. Você pode até perguntar “mas
qual é o real problema do debbuing em um ambiente multi thread?”. Imagine a seguinte situação, você
possui uma thread que está em execução contínua (monitorando um diretório por exemplo), enquanto
esta thread é executada, você inicia a depuração de outra rotina qualquer executada pela main thread.
De repente, sem a menor explicação, o ponteiro do seu debug vai para outra rotina em outra unit sem a
menor relação com a depuração que você estava fazendo. Quando você consegue se dar conta do que
aconteceu, percebe que o ponteiro da depuração foi movido para o processamento da thread. Quem
está familiarizado com um ambiente multi thread conhece bem esse tipo de história.
Essa janela mostra todas as threads da aplicação, assim como seu estado, status, etc. Por padrão, ao
entrar em modo de debugging o próprio Delphi já abre esta janela no rodapé da tela. Esta imagem foi
capturada de uma aplicação onde, além da main thread, eu tinha mais 3 threads em execução. Ainda
assim, mesmo com a janela thread status aberta, não ajuda muito, pois não sei qual thread é qual. Para
resolver esse problema, podemos identificar as threads em debugging através do
método NameThreadForDebugging() da classe TThread. Esse método pode ser utilizado no execute da
thread:
1 procedure TArquivoTextoInThread.Execute;
2 begin
3 Self.NameThreadForDebugging(Format('arquivo txt %d',[self.FLinhasAImprimir]));
4 Self.escreverArquivoDeTexto();
5 end;
Inclusive é possível nomear a main thread, basta utilizar um cast com a classe TThread. Por exemplo:
Caso você consiga identificar uma thread pelo id, você pode alterar seu nome para debugging na janela
thread status Basta clicar com o botão direito do mouse sobre a linha da thread e depois selecionar a
opção name thread.
Agora, imagine que você queira depurar uma determinada thread, mas não quer que a execução de
outra thread qualquer possa interferir nesse processo. Em uma situação destas você pode parar a
execução de uma ou todas as threads. Quando a aplicação tem a sua execução pausada (caindo em um
breakpoint ou caso o botão de pause seja clicado), você pode clicar com o botão direito do mouse sobre
a thread que deseja pausar a execução e selecionar a opção Freeze, ou pode clicar com o botão direito
do mouse sobre a única thread que você deseja que continue executando e se selecionar a opção Freeze
All Other Threads.
Veja que estou com o ponteiro de depuração em um breakpoint de uma thread (status Breakpoint) e
parei a execução de outra thread (status Frozen). Caso eu passe a depurar a thread (com o F8 ou F7 por
exemplo) o status mudaria de Breakpoint para Stepped. Para liberar a execução da thread congelada
(Frozen), basta clicar nela e selecionar a opção Thaw.
Por falar em breakpoints, você pode condicionar um breakpoint para que ele funcione somente quando
aquele trecho de código seja executado por uma determinada thread. Para realizar essa configuração,
clique com o botão direito do mouse sobre o breakpoint e selecione a opção Breakpoint Properties, uma
das propriedades é exatamente a thread:
Isso garante que o breakpoint só será acionado, caso a thread corrente seja a thread configurada.
Espero que tenha ajudando um pouco com essas dicas, apesar de threads não serem nenhuma
novidade, vejo que muitas vezes é um assunto que não recebe o seu devido valor. Obviamente o
assunto não se encerra aqui, até mesmo porque esse estudo recebeu o nome de Threads no Delphi, por
onde começar? Sendo assim, isso é só o começo.