Mostrando postagens com marcador Threads. Mostrar todas as postagens
Mostrando postagens com marcador Threads. Mostrar todas as postagens

22 de outubro de 2009

Obtendo informações sobre as Threads de um processo

Recebi um feedback baseado no que publiquei no último post (Obtendo informações sobre processos em execução) perguntando se é possível fazer a mesma coisa com Threads. Isto é, dado um handle para um processo é possível obter informações sobre as Threads criadas por ele ?

A resposta é sim, é possível obter uma lista das Threads que estão associadas a um determinado processo. O método para obter esta lista, no entanto, difere um pouco daquele que usei para obter a lista de processos. O problema é que a criação de Threads por um programa é dinâmica e o números de Threads existentes num momento pode não ser mais o mesmo no momento seguinte, dificultando a obtenção de uma lista. (Ok, a lista de processos ativos também pode variar bastante de um momento para outro; volto nesse tópico mais abaixo.)

Para contornar esse problema, a Microsoft disponibilizou na API do Windows uma função chamada CreateToolhelp32Snapshot cujo objetivo é tirar um "instantâneo" do estado de um processo, aí incluindo informações sobre as Threads existentes no momento da chamada da função. No Delphi 2005, ela pode ser encontrada na unit TlHelp32 e possui a seguinte declaração:
function CreateToolhelp32Snapshot(dwFlags, th32ProcessID: DWORD): THandle;

Quais informações sobre o processo serão salvas por esta função depende do valor passado no parâmetro dwFlags . No caso de Threads, há um porém. Especificando a flag TH32CS_SNAPTHREAD, a função ignora o parâmetro com a identificação do processo e captura informações sobre todas as Threads existentes no Windows no momento da execução.
Para percorrer a lista de Threads, há duas outras funções na mesma unit, que são Thread32First (para obter informações sobre a primeira thread) e Thread32Next (para continuar a busca, obtendo as demais). Como estas funções listarão todas as Threads do sistema, é preciso pinçar as informações que interessam. Para isso, use a estrutura THREADENTRY32 (preenchida como retorno em ambas as funções) para comparar a identificação do processo que criou a Thread com a identificação do processo que você deseja. Veja abaixo uma forma comum de usar essas funções:
procedure TForm1.Button1Click(Sender: TObject);
var hSnap, procId : THandle;
cont : integer;
continua : boolean;
threadInfo: TThreadEntry32;
begin
hSnap := CreateToolhelp32Snapshot (TH32CS_SNAPTHREAD, 0);

procId := GetCurrentProcessId();

if hSnap <> INVALID_HANDLE_VALUE then
begin
cont := 0;
threadInfo.dwSize := sizeof (threadInfo);
continua := Thread32First(hSnap, threadInfo);
while continua do
begin
if procId = threadInfo.th32OwnerProcessID then
Inc (cont);
continua := Thread32Next(hSnap, threadInfo);
end;
end;

CloseHandle (hSnap);
end;

Neste exemplo, eu apenas conto quantas threads foram criadas pelo meu próprio programa. Faço isso recuperando o ID do meu processo com GetCurrentProcessId e comparando este ID com aquele contido no campo th32OwnerProcessID da estrutura associada à Thread. Este campo indica qual foi o processo que criou a Thread em questão.

Poderia ainda ter usado a identificação de cada Thread, conforme alimentado no campo th32ThreadID, para obter mais informações ou executar alguma das operações disponíveis na API do Windows para Processos e Threads.

Quando não for mais precisar do snapshot obtido pelo CreateToolhelp32Snapshot, libere os recursos associados a ele através de uma chamada à função CloseHandle.

Usando essa mesma técnica, podemos conseguir a lista dos processos em execução no Windows. Para isso, use as funções Process32First e Process32Next.

8 de outubro de 2009

Comunicação com a Thread principal usando mensagens do Windows

No fonte do programa que eu publiquei no post sobre sincronização de Threads eu usei o recurso de enviar mensagens do Windows para comunicação com a Thread principal, isto é, aquela Thread onde a janela do programa é controlada. Para não misturar as técnicas, não toquei nesse assunto lá mas vou explicar aqui como é que funciona o mecanismo.

O advento da programação visual - onde você simplesmente arrasta botões, caixas de edição e outros componentes para desenhar as janelas de uma aplicação e responder a eventos associdados a eles - escondeu do programador um fato básico a respeito do funcionamento do Windows : tudo em uma janela acontece através do envio de mensagens. Desde a criação e destruição da janela, passando pela pintura e alterações no tamanho até o clique de botões, digitação de textos e mesmo o simples passar do mouse por sobre a janela geram uma infinidade de eventos chamados de "Mensagens".

Cada janela é criada com uma fila interna onde as mensagens recebidas são colocadas pelo Windows para que a janela possa recuperá-las e tratá-las. Por isso, nos primórdios da programação para Windows, todo programa era montado em cima de um laço para obter cada mensagem e uma estrutura do tipo switch/case para tratar cada uma delas - muitas tratadas automaticamente pelo próprio Windows, como mover ou redimensionar a janela. Hoje, esse mecanismo continua existindo mas é encapsulado por classes dentro de frameworks como a VCL ou o .NET.

Uma mensagem é constituida de um código que a define (ex: WM_PAINT quando a janela precisa ser desenhada ou WM_MOUSEMOVE quando passamos o mouse sobre uma janela), o handle que indica a qual janela a mensagem é direcionada e parâmetros com informações extras para complementar a descrição do evento. O significado dos parâmetros varia de acordo com a mensagem.

Além das mensagens definidas pelo próprio sistema operacional, é permitido às aplicações registrar tipos de mensagens privadas, cujo siginificado é restrito ao escopo da própria aplicação. Foi isso que fiz no programa de exemplo das Threads, declarando duas novas mensagens:
var
WM_NOTIFY_THREAD_STATUS : Cardinal;
WM_NOTIFY_THREAD_TERMINATED : Cardinal;

{ ... }

WM_NOTIFY_THREAD_STATUS := RegisterWindowMessage ('WM_NOTIFY_THREAD_STATUS');
WM_NOTIFY_THREAD_TERMINATED := RegisterWindowMessage ('WM_NOTIFY_THREAD_TERMINATED');

O Windows atribuirá ao nome passado para a função RegisterWindowMessage um código interno de modo que novas chamadas a ela sempre retornarão o mesmo valor. Não é o caso do exemplo mas eu poderia ter usado esse mecanismo para comunicar com outro programa meu em execução no mesmo computador e que registrasse uma mensagem com o mesmo nome.

Com a mensagem registrada, minha Thread pode notificar o seu status à janela principal enviando a ela uma mensagem. Cada Thread de classificação no exemplo é associada a uma estrutura chamada _Status que, entre outras coisas, possui o handle da janela do programa, usado no envio da mensagem.
PostMessage (_Status.OwnerForm.Handle,
WM_NOTIFY_THREAD_STATUS,
Integer (_Status), 0);

A função PostMessage da API do Windows coloca uma mensagem na fila de mensagens da janela indicada pelo Handle mas não aguarda seu tratamento. Os demais parâmetros são o código da mensagem e informações adicionais específicas dessa mensagem - no caso, o ponteiro para toda a estrutura de status da Thread.

No caso do Delphi e do C++ Builder, o tratamento de mensagens direcionadas a uma janela pode ser estendido criando uma sobreposição da função WndProc, que existe em todas as janelas.
{ Definição na classe da Janela }
protected
procedure WndProc(var Message: TMessage);override;

{ ... }

{ Corpo da função }
procedure TWSyncThread.WndProc(var Message: TMessage);
var lMsg : TWMsgNotifyStatus;
begin
if Message.Msg = WM_NOTIFY_THREAD_STATUS then
begin
lMsg := TWMsgNotifyStatus(Message);
lblMsg.Caption := lMsg.Status.Texto;

{ ... }

lMsg.Result := -1;
end else
inherited;
end;

Veja o uso da palavra-chave inherited. Isto repassa o tratamento de todas as outras mensagens à mesma função existente na classe pai (a própria classe TForm). Sem isso, a janela deixa de funcionar.

A estrutura TWMsgNotifyStatus é definida por mim para conter os parâmetros esperados por mensagens do tipo WM_NOTIFY_THREAD_STATUS. A estrutura é construída de forma que os parâmetros passados à função PostMessage se encaixem nela, facilitando o acesso e a interpretação do significado de cada parâmetro.

7 de outubro de 2009

Trabalhando com Threads em Delphi - Sincronização com Eventos - parte 2

Para mostrar um exemplo prático dos conceitos de que falei na primeira parte da sincronização de threads com eventos, vou trabalhar com o seguinte cenário : um programa que tenha 4 listas distintas com uma quantidade aleatório de items a serem ordenados. Cada lista será montada e ordenada em uma thread separada, simulando que as listas vieram de fontes diferentes. Como elas têm uma quantidade diferente de itens, haverá uma thread especial, desenhada apenas para aguardar o términio das demais. O efeito disso é que as threads estarão sincronizadas e o programa poderá prosseguir com sua execução nesse ponto com as 4 listas ordenadas. Veja novamente o esquema geral para esse cenário.
Threads com eventos

Para implementar essa solução em Delphi, eu comecei criando uma unit que centralizará o controle da sincronização. Ela terá uma variável global para representar o evento "Término das threads de Classificação" e funções para incrementar e decrementar a quantidade de Threads ativas. O objetivo é que cada nova thread de classificação que for criada incremente um contador interno e, quando essa Thread terminar, o contador seja decrementado. Quando ele atingir o valor "zero", o evento será acionado para avisar que as listas já estão ordenadas:
var _QtThreads : Integer;
_CS_QtThreads : TCriticalSection;

procedure IncQtThreads;
begin
{ Incrementa o contador global de threads. Usa a seção crítica para proteção já que várias threads podem tentar o acesso simultaneamente }
_CS_QtThreads.Acquire;

try // Por segurança, trata exceções
Inc(_QtThreads);
finally
_CS_QtThreads.Release;
end;
end;

procedure DecQtThreads;
begin
{ Decrementa o contador global de threads. Usa a seção crítica para proteção já que várias threads podem tentar o acesso simultaneamente }
_CS_QtThreads.Acquire;

try
Dec(_QtThreads);

{ Se Chegou a zero, significa que as threads de classificação já terminaram a execução. Então, notifica a ocorrência do evento p/ quem estiver aguardando }
if _QtThreads = 0 then
evtThreadsSort.SetEvent;
finally
_CS_QtThreads.Release;
end;
end;

A linha evtThreadsSort.SetEvent notifica que o evento esperado ocorreu. Então, na classe que representa a Thread aguardando, uso a variável do evento para esperar pela notificação:
procedure TWThrEsperaSort.Execute;
var res : TWaitResult;
begin
{ Espera no máximo 1 minuto e meio pelo evento }
res := evtThreadsSort.WaitFor(90000);
{ Se tudo correu bem, neste ponto as listas já estão ordenadas. }

Este código suspende a execução da Thread enquanto aguarda. É por essa razão que eu não uso a Thread principal para aguardar; se fizesse isso, o usuário perderia a interação com o Form durante a espera, dando a impressão que o programa todo "travou". Essa Thread só continuará sua execução quando o evento esperado ocorrer ou se o tempo de espera (timeout) expirar ou ainda se houver algum erro inesperado.

Se tudo correu bem, as listas estarão classificadas no ponto do código imediatamente após a chamada ao WaitFor e o processo que depende da classificação de todas as listas pode continuar.

Eu criei uma classe de Thread para realizar a classificação de listas e, no construtor dela, faço o incremento do contador de threads ativas. O decremento desse valor é chamado na função de finalização.
Constructor TWThrSort.Create (PStatus: TWThreadStatus);
begin
inherited Create (true);
IncQtThreads;
FreeOnTerminate := true;
OnTerminate := OnThreadFinish;
{ ... }
end;

procedure TWThrSort.OnThreadFinish(Sender: TObject);
begin
{ ... }
DecQtThreads;
{ ... }

Duas coisas a observar no trecho acima. Quando chamo o construtor herdado Create, informo True no parâmetro createSuspended, o que cria a Thread em estado suspenso. Isto significa que terei que iniciá-la manualmente. O outro ponto é que ajustei a propriedade FreeOnTerminate para True, indicando que a finalização ocorrerá automaticamente quando encerrar a função Execute.

A ligação disso tudo se dá no ponto em que as Threads de classificação são instanciadas. Neste caso, inclui o código abaixo como resposta ao clique de um botão no meu Form:
{ Reseta o evento para garantir que está no estado inicial }
evtThreadsSort.ResetEvent;

{ Cria a thread de espera passando o handle desse Form }
TWThrEsperaSort.Create(Handle);

{ Cria as threads de classificação }
for i := 0 to 3 do begin
lStatus := TWThreadStatus.Create;
{ ... }
thds[i] := TWThrSort.Create(lStatus);
end;
{ sincroniza o início das threads de classificação }
for i := 0 to 3 do
thds[i].Resume;
{ ... }

É preciso colocar a variável do Evento em seu estado inicial (ResetEvent) antes de criar a thread de espera e só então podemos criar as demais threads, dedicadas à classificação das listas.

Mas, porque iniciar as Threads manualmente neste caso? Se eu deixar a criação da Thread iniciá-la automaticamente e a primeira Thread for rápida o suficiente, ela poderia terminar antes da segunda Thread ter tempo de incrementar o contador, o que, por sua vez, acionaria o Evento antes da hora pois o contador das threads ativas voltaria ao valor zero. Ao fazer a criação e a execução em pontos distintos, o contador de threads ativas estará em seu valor máximo quando as threads se iniciarem. Assim, mesmo que a primeira Thread execute muito rápido, o contador só voltará ao valor zero quando todas terminarem, acionando o Evento no momento apropriado.

As classes TEvent e TCriticalSection encapsulam chamadas a funções da API do Windows para trabalhar com sincronização. Essas funções podem ser chamadas diretamente, se for necessário. A documentação para elas pode ser encontrada no site MSDN.

Para fazer o download código fonte do exemplo em Delphi 2005, clique aqui.

2 de outubro de 2009

Trabalhando com Threads em Delphi - Sincronização com Eventos - parte 1

Um dos aspectos mais interessantes a respeito do uso de threads (ou linhas de execução) em um programa é a possibilidade que se abre para divisão real de tarefas dentro do programa. Se, para resolver um problema computacional, é possível dividir uma tarefa grande em passos menores que podem ser executados simultaneamente, então a resolução desse problema pode ser implementada com threads. Num computador com múltiplos núcleos (CPUs), essa solução significará uma execução mais rápida, com um tempo de resposta menor já que a execução das threads nesse ambiente será de fato simultânea.

Uma situação real em que isso pode ser aplicado é quando você tem massas de dados heterogênas (vindas de fontes diferentes) e que precisam ser ordenadas antes de serem utilizadas como se viessem uma única fonte.

Logo de cara, podemos identificar um grande problema : eu não sei de antemão o tamanho dessas fontes de dados, isto é, quantos registros poderão vir de cada fonte. Só é possível prosseguir com o processamento após todas as threads terminarem a sua parte na busca e ordenação. Assim, certamente teremos que sincronizar essas threads para que o uso da lista final ordenada só ocorra depois do fim da última thread de ordenação.

O mecanismo que os sistemas operacionais disponibilizam para esse fim é chamado Evento. No Delphi, a classe TEvent implementa esse mecanismo, encapsulando chamadas à API do Windows. Um objeto do tipo Evento é um objeto de sincronização cujo estado pode ser explicitamente sinalizado, isto é, o programador pode indicar manualmente que ocorreu um determinado evento que outras threads estejam aguardando.

Em termos de programação, é preciso criar um objeto global do tipo Evento e dar-lhe um nome conhecido que o diferencie de outros Eventos que possam existir. O objeto tem que ser global - e não parte de uma thread particular - porque ele terá que ser acessível por outras threads do programa. Ao dar início a cada uma das threads que queremos aguardar, o programa incrementa um contador. Uma thread em especial, então, ficará em estado suspenso, aguardando a notificação de ocorrência do evento. Conforme cada thread vai sendo finalizada, o contador é decrementado. Na última delas, o contador chegará a zero e a thread pode emitir a notificação de que todas já completaram seus respectivos processamentos e o fluxo de execução da tarefa pode continuar. Veja a representação desse funcionamento no gráfico abaixo:
Threads com eventos

A thread que vai aguardar a sinalização pode ser a principal do seu programa. Entretanto, isso não é recomendável porque é a thread principal quem recebe as mensagens do Windows, incluindo aquelas resultantes de interação com o usuário. Portanto, ao ter sua execução suspensa, o programa perderá interatividade e dará ao usuário a impressão de que está "travado".

É claro que neste tipo de cenário devemos dobrar a atenção no tratamento de erros e exceções no processamento das threads, caso contrário a sinalização do evento pode nunca ocorrer e o programa aguardará indefinidamente.

Volto num próximo post com código em Delphi para implementar o exemplo.

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.

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.

17 de agosto de 2009

Trabalhando com Threads

Uma Thread - ou Linha de Execução - é a unidade básica de execução de programas dos Sistemas Operacionais modernos, tais como Windows e Linux. Isso significa que tais Sistemas Operacionais controlam o uso da CPU por Thread, alocando um determinado tempo para cada uma delas ter a oportunidade de executar seu código. Essa forma de trabalhar dá ao usuário a sensação de que todos os programas estão executando ao mesmo tempo quando, na verdade, cada um deles executa apenas por uma pequena fatia de tempo (time slice).

Quando executamos um programa, o Sistema Operacional se encarrega de criar a Thread principal. Se for necessário ou desejado, temos que criar as demais Threads manualmente. Todas as Threads criadas por um processo (programa) compartilham uma mesma área de memória chamada de address space e, por isso, elas conseguem acessar as mesmas variáveis. É fácil perceber a bagunça que vai virar se uma Thread gravar um valor numa variável e a Thread seguinte fizer a mesma coisa quando tiver seu tempo de CPU. O exemplo foi com uma variável mas podia ser um arquivo, uma impressora compartilhada ou outro recurso qualquer do Sistema Operacional.

Então, por que criar uma outra Thread no meu programa e ter que arcar também com a manutenção de um controle para acessar os recursos compartilhados ? Primeiro, o controle de acesso não é tão complexo. Segundo, há várias situações que justificam ter outras linhas de execução num programa. Explico abaixo três dessas situações:
Tempo de Resposta. Imagine que você está construindo uma aplicação para a internet que poderá receber milhares de acessos simultâneos. Há um ponto nesse seu programa que recebe todas as solicitações enviadas pela internet, processa a solicitação e devolve um documento HTML com a resposta. Se você tiver apenas uma linha de execução, apenas uma solicitação da internet será tratada por vez; todas as demais terão que aguardar, não importa o tempo que a solicitação atual demore. Criar novas Threads para tratar solicitações distintas melhorará o tempo de resposta já que cada um terá que aguardar apenas o processamento de sua própria solicitação.

Divisão de Tarefas.Se você tem um problema grande cuja solução computacional pode ser dividida em passos menores e parte desses passos pode ser calculado de forma independente, então criar Threads melhorará a performance de seu algorítmo. Um exemplo seria você ter que mesclar duas massas de dados vindas de lugares diferentes para produzir uma nova massa ordenada. Ordenar os dois conjuntos são passos que poderiam ser executados simultaneamente (em Threads separadas) para agilizar o resultado.

Monitoramento de Eventos.Há situações em que um programa precisa se manter responsivo mesmo quando está aguardando a ocorrência de algum evento. Por exemplo, um programa que tenha que aguardar a criação de certos arquivos mas que nesse meio tempo possa também continuar interagindo com o usuário. No Windows, as funções que monitoram a criação de arquivos travam a linha de execução até que haja a mudança monitorada ou que ocorra um timeout. Se o monitoramento estiver numa Thread separada, o programa poderá continuar interagindo normalmente com o usuário.
Para criar uma nova Thread em Delphi ou C++ Builder, basta criar uma herança da classe TThread - acessando o menu File -> New -> Other -> Thread Object, o próprio IDE prepara uma unit com o esqueleto para a nova classe. Cada instância dessa classe produz uma nova linha de execução para o programa.

Basicamente, é preciso sobrescrever o método Execute - ele é abstrato na classe base TThread - e colocar nele os comandos a serem executados pela nova linha de execução. Em Delphi:
procedure TWMinhaThread.Execute();
begin
b>while not Terminated do
begin
{ Código a ser executado vai aqui...}
end;
end;
É bastante comum que o código colocado no Execute tenha um laço while testando o valor da propriedade Terminated, mas não é uma obrigação. No entanto, essa propriedade registra se alguma outra parte do programa solicitou o encerramento da Thread - por exemplo, se o usuário cancelou uma operação demorada. É, portanto, uma boa prática de programação verificá-la em pontos estratégicos do código da Thread.

Outras propriedades e métodos interessantes da classe TThread:
Propriedade Priority. Determina a prioridade de execução de uma Thread em relação às outras threads do mesmo processo (programa). Quanto maior a prioridade, maior é o tempo de CPU destinado à thread. Alterações nesta propriedade devem ser feitas com critério pois aumentar a prioridade de uma thread que tenha processamento pesado pode prejudicar a responsividade das demais, enquanto diminuir a prioridade de uma Thread atrasará sua própria execução. Uma boa regra é manter a prioridade com seu valor padrão e só aumentá-lo em threads que sejam de fato mais críticas, como por exemplo uma que esteja aguardando um evento externo importante para o processo ou que seja classificada como "missão crítica". Diminua a prioridade em threads que realizem tarefas cujos resultados não estejam sendo aguardados pelo usuário, como por exemplo, aquela que arquiva emails antigos no Outlook.

Propriedade FreeOnTerminate. Determina se a VCL destruirá de forma automática a instância da classe de Thread. Se estiver ajustada com true, a VCL chamará automaticamente o destrutor da classe para a instância assim que o método Execute terminar. Neste caso, evite manter uma variável que contenha a instância da classe a menos que você possa garantir com certeza absoluta que a instância é válida nos momentos em que você precisar acessá-la. Se estiver ajustada com false, você será responsável por chamar o destrutor no momento que for mais apropriado para a aplicação.

Método Resume. Há um parâmetro no construtor da classe TThread que indica se a thread em questão será criada em estado suspenso ou se iniciará a execução imediatamente. Se solicitar que a thread esteja com a execução suspensa, você pode comandar o início da execução chamando o método Resume. Este método dará início à nova thread e executará o código que você escreveu no Execute.

Método Terminate. Use esse método para notificar a thread que ela deve encerrar sua execução. Isso é feito ajustando o valor da propriedade Terminated para true; daí a importância de testar o Terminated em pontos estratégicos da execução da thread.
No próximo post, mostro um exemplo concreto com o básico sobre o uso de Threads em Delphi.