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.

6 comentários :

Anônimo disse...

muito boa sua explicação. parabéns.

Thiago Diniz disse...

Perfeito, parabéns...

Anônimo disse...

Muito bom mesmo! Parabéns pelo trabalho!

João Carlos Cordeiro disse...

excelente .. parabéns

Nada do Amaral disse...

Enviar um e-mail a partir de um ERP e continuar trabalhando seria um exemplo de uso?

Luís Gustavo Fabbro disse...

Sim, essa é uma situação na qual o uso de threads se encaixa perfeitamente.

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