Difference between revisions of "Multithreaded Application Tutorial/pt"

From Lazarus wiki
Jump to navigationJump to search
Line 295: Line 295:
  
 
  rm -f Log* && ./project1
 
  rm -f Log* && ./project1
 +
 +
=== Linux ===
 +
 +
Se você tentar depurar um aplicativo multi-tarefa em Linux, você terá um grande problema: o servidor X irá travar.
 +
Não se sabe como resolver isso corretamente, mas uma solução é:
 +
 +
Crie uma nova instância do X:
 +
 +
X :1 &
 +
 +
Será aberta uma nova sessão X, e quando você mudar para a nova sessão X (para retornar ao que você estava trabalhando pressione CTRL+ALT+F7), você será capaz de voltar para a nova tela gráfica usando CTRL+ALT+F8 (Se essa combinação não funcionar, tente com CTRL+ALT+F2 ...isto se estiver trabalhado com Slackware)
 +
 +
Então você poderia, se quiser, criar uma área de trabalho na sessão X iniciada com:
 +
 +
gnome-session --display=:1 &
 +
 +
Então, no Lazaro, nos parâmetros de execução do projeto, verifique “Use display” e digite :1.
 +
Agora a aplicação será executada no segundo servidor X e depurada no primeiro.
 +
Isto foi testado com Free Pascal 2.0 e Lazarus 0.9.10 no Windows e Linux.
 +
 +
----
 +
 +
Em vez de criar uma nova sessão X, pode-se usar Xnest. Xnest é uma sessão X em uma janela. Usando isto o servidor X não travará durante a depuração das threads, e é muito mais fácil de depurar sem ter que alternar entre os terminais o tempo todo.
 +
 +
A linha de comando para executar Xnest é
 +
 +
Xnest :1 -ac
 +
 +
para criar uma sessão X sobre :1, e desabilitar os controles de acesso.

Revision as of 23:05, 25 September 2009

Deutsch (de) English (en) español (es) français (fr) 日本語 (ja) polski (pl) português (pt) русский (ru) slovenčina (sk) 中文(中国大陆)‎ (zh_CN)

Introdução

Esta página irá tentar explicar com escrever e debugar uma aplicação multi-tarefa(multithread) com Free Pascal e Lazarus.

Uma aplicação multi-tarefa(multithread) é uma que cria duas ou mais tarefas em execução que trabalham ao mesmo tempo.

Se você é novo em multi-tarefa, por favor leia o parágrafo "O que você necessita para multi-tarefa ?" para descobrir, se você realmente necessita disto. Você pode se salvar de um monte de dores de cabeça.


Uma das tarefas é chamada de tarefa princípal. A Tarefa Princípal é criada pelo sistema operacional, cada vez que nossa aplicação inicia. A Tarefa Principal deve ter somente tarefa que atualiza os componentes que faz interfaces com o usuário(senão, a aplicação pode cair).

A principal idéia é que a aplicação pode fazer algum processamento em plano de fundo(background - numa segunda tarefa) enquando o usuário pode continuar seu trabalho (usando a tarefa principal).

Outro uso das tarefas é somente para ter uma melhor resposta da aplicação. Se você cria uma aplicação, e quando o usuário pressiona um botão, a aplicação inicia o processamento (um grande trabalho) ... e enquanto processando, a tela para de responder, e dá ao usuário a impressão de que a aplicação está morta, que não é legal. Se o grande trabalho é executado numa segunda tarefa, a aplicação mantém-se respondendo(sempre) como se estisse inativa. Neste caso é uma boa idéia, antes iniciando a tarefa, para disabilitar os botões da janela para evitar o usuário iniciar mais que uma tarefa por trabalho.

Outro uso, é para criar uma aplicação servidora(server application) que é abilitada para responder a vários clientes ao mesmo tempo.

Você necessita de multi-tarefa?

Se você é um novato em multi-tarefa e somente você quer fazer sua aplicação com melhor tempo de resposta enquanto sua aplicação processa grandes trabalhos, então multi-tarefa pode não ser, o eu você está procurando.

Aplicações multi-tarefa sempre tem pesados debugs e eles são cada vez mais complexos. E em muitos casos você não precisa de multi-tarefa. Uma simples tarefa é o suficiente.

Multi-tarefa somente é necessário para:

  • bloquear handles, como comunicação na rede
  • usando múltiplos processadores de uma vez
  • algoritmos e chamadas de bibliotecas, que não podem ser separadas em pequenas partes.

A classe TThread

Os exemplos a seguir podem ser encontrados no diretório examples/multithreading/.

Para criar um aplicação multi-tarefa, a maneira mais fácil e usando a classe TThread. Esta classe permite a criação de uma thread adicional (ao lado da thread principal) de uma maneira simples. Normalmente você é obrigado a substituir apenas dois métodos: o construtor Create, e o método Execute. No construtor, você vai prepara a thread para funcionar. Você vai definir os valores iniciais das variáveis ou propriedades que você precisa. O construtor original da TThread requer um parâmetro chamado Suspended. Como você pode esperar, o ajuste Suspended = True impedirá que a thread execute automaticamente após a criação. Se Suspended = False, a thread irá executar logo após a criação. Se a thread é criada com Suspended = True, então ele será executado apenas depois que o método Resume for chamado.

A partir da versão 2.0.1 do FPC, TThread.Create tem um parâmetro implícito para o tamanho da pilha. Agora você pode alterar o tamanho predefinido de cada pilha, para cada thread que você criar, se você precisar. Procedimentos de chamadas recursivas é um bom exemplo. Se você não especificar o parâmetro de tamanho da pilha, o tamanho padrão da pilha do OS será usado.

Na substituição do método Execute, você escreverá o código que irá funcionar na thread.

A classe TThread tem uma importante propriedade: Terminated : boolean; Se a thread tem um loop ( isto é típico), o loop deve ser terminado quando Terminated for True (este tem valor False porpadrão). Por cada passagem, o valor do Terminated deve ser verificado e, se for True, então o loop deve ser encerrado tão rapidamente como é apropriado, após alguma limpeza necessária. Tenha em mente que o método Terminate não faz nada por padrão: o método Execute deve explicitamente implementar o suporte para ele finalizar o seu trabalho.

Como já explicamos anteriormente, a thread não deve interagir com componentes visuais. Atualizações para componentes visíveis deve ser feita dentro do contexto da thread principal. Para fazer isto, existe um método em TThread chamado Synchronize. Synchronize requer um método (este não leva nenhum parâmetro) como um argumento. Quando você chamar esse método através Synchronize(@MyMethod) a execução do thread será pausada, o código de MyMethod funcionará na thread principal, e a execução da thread será recomeçada então. O funcionamento exato de Synchronize depende da plataforma, mas basicamente é isto: afixa uma mensagem na fila de mensagem principal e vai dormir. Eventualmente a thread principal processa a mensagem e chama MyMethod. Desta forma MyMethod é chamado sem contexto, que não significa que seja durante um evento do mouse ou durante o evento Paint, mas depois. Após a thread principal ter executado MyMethod, desperta e processa a próxima mensagem. A thread então continua.

Há uma outra propriedade importante de TThread: FreeOnTerminate. Se esta propriedade é True, o objeto da thread é automaticamente liberado quando a execução da thread (método Execute) terminar. Se não a aplicação precisará ser liberada manualmente.

Exemplo:

<delphi>  

 Type
   TMyThread = class(TThread)
   private
     fStatusText : string;
     procedure ShowStatus;
   protected
     procedure Execute; override;
   public
     Constructor Create(CreateSuspended : boolean);
   end;
 constructor TMyThread.Create(CreateSuspended : boolean);
 begin
   FreeOnTerminate := True;
   inherited Create(CreateSuspended);
 end;
 procedure TMyThread.ShowStatus;
 // this method is executed by the mainthread and can therefore access all GUI elements.
 begin
   Form1.Caption := fStatusText;
 end;
 procedure TMyThread.Execute;
 var
   newStatus : string;
 begin
   fStatusText := 'TMyThread Starting...';
   Synchronize(@Showstatus);
   fStatusText := 'TMyThread Running...';
   while (not Terminated) and ([any condition required]) do
     begin
       ...
       [here goes the code of the main thread loop]
       ...
       if NewStatus <> fStatusText then
         begin
           fStatusText := newStatus;
           Synchronize(@Showstatus);
         end;
     end;
 end;

</delphi>

Na aplicação:

<delphi>

 var
   MyThread : TMyThread;
 begin
   MyThread := TMyThread.Create(True); // This way it doesn't start automatically
   ...
   [Here the code initialises anything required before the threads starts executing]
   ...
   MyThread.Resume;
 end;

</delphi>

Se você quer fazer sua aplicação mais flexível, você pode criar um evento para o thread; Desta forma o seu método sincronizado não será fortemente acoplado com um formulário ou classe específico: você pode anexar os ouvintes de eventos da thread.

Aqui está um exemplo:

<delphi>  

 Type
   TShowStatusEvent = procedure(Status: String) of Object;

 

   TMyThread = class(TThread)
   private
     fStatusText : string;
     FOnShowStatus: TShowStatusEvent;
     procedure ShowStatus;
   protected
     procedure Execute; override;
   public
     Constructor Create(CreateSuspended : boolean);
     property OnShowStatus: TShowStatusEvent read FOnShowStatus write FOnShowStatus;
   end;

 

 constructor TMyThread.Create(CreateSuspended : boolean);
 begin
   FreeOnTerminate := True;
   inherited Create(CreateSuspended);
 end;

 

 procedure TMyThread.ShowStatus;
 // this method is executed by the mainthread and can therefore access all GUI elements.
 begin
   if Assigned(FOnShowStatus) then
   begin
     FOnShowStatus(fStatusText);
   end;
 end;

 

 procedure TMyThread.Execute;
 var
   newStatus : string;
 begin
   fStatusText := 'TMyThread Starting...';
   Synchronize(@Showstatus);
   fStatusText := 'TMyThread Running...';
   while (not Terminated) and ([any condition required]) do
     begin
       ...
       [here goes the code of the main thread loop]
       ...
       if NewStatus <> fStatusText then
         begin
           fStatusText := newStatus;
           Synchronize(@Showstatus);
         end;
     end;
 end;

</delphi>

Na Aplicação,

<delphi>

 Type
   TForm1 = class(TForm)
     Button1: TButton;
     Label1: TLabel;
     procedure FormCreate(Sender: TObject);
     procedure FormDestroy(Sender: TObject);
   private
     { private declarations }
     MyThread: TMyThread; 
     procedure ShowStatus(Status: string);
   public
     { public declarations }
   end;

 

 procedure TForm1.FormCreate(Sender: TObject);
 begin
   inherited;
   MyThread := TMyThread.Create(true);
   MyThread.OnShowStatus := @ShowStatus;
 end;

 

 procedure TForm1.FormDestroy(Sender: TObject);
 begin
   MyThread.Terminate;
   MyThread.Free;
   inherited;
 end;

 

 procedure TForm1.Button1Click(Sender: TObject);
 begin
  MyThread.Resume;
 end;

 

 procedure TForm1.ShowStatus(Status: string);
 begin
   Label1.Caption := Status;
 end;

</delphi>

Coisas para ter cuidado especiais

No Windows há um possível problema quando se usa threads e você usar a opção -Ct (checagem de pilha). Por razões não tão claras a verificação de pilhas será disparada em qualquer TThread.Create, se você usar o padrão de tamanho da pilha. A única alternativa no momento é simplesmente não usar a opção -Ct. Nota-se que não causa uma exceção no segmento principal, mas no recém-criado. É como se a thread não tivesse iniciado.

Um bom código para verificar esta e outras exceções que podem ocorrer na criação da threads é:

<delphi>

   MyThread:=TThread.Create(False);
   if Assigned(MyThread.FatalException) then
     raise MyThread.FatalException;

</delphi>

Esse código vai garantir que qualquer exceção que ocorra durante a criação da thread seja lançada em seu segmento principal.

Unidades necessárias para uma aplicação multi-tarefa

Você não precisa de qualquer unidade especial para funcionar no Windows. Contudo com Linux, Mac OS X e FreeBSD, você vai precisar da unidade cthreads e deve ser a primeira unidade do projeto. (a unit do programa, .lpr)! Assim, o código do aplicativo Lazaro deve ser parecido:

<delphi>

program MyMultiThreadedProgram;
{$mode objfpc}{$H+}
uses
{$ifdef unix}
  cthreads,
  cmem, // the c memory manager is on some systems much faster for multi-threading
{$endif}
  Interfaces, // this includes the LCL widgetset
  Forms
  { add your units here },

</delphi>

Se você esquecer isso, você irá obter este erro na inicialização:

This binary has no thread support compiled in.
Recompile the application with a thread-driver in the program uses clause before other units using thread.

Pacotes que utilizam multi-tarefa devem adicionar o indicador -dUseCThreads nas opções de uso personalizado. Abra o pacote, e no editor vá em Options > Usage > Custom e adicione -dUseCThreads. Isto irá definir este indicador para todos os projetos e pacotes usando este pacote, incluindo a IDE. A IDE e todas as novas aplicações criadas já terá o seguinte código incluído no arquivo .lpr:

<delphi>

uses
  {$IFDEF UNIX}{$IFDEF UseCThreads}
  cthreads,
  cmem, // the c memory manager is on some systems much faster for multi-threading
  {$ENDIF}{$ENDIF}

</delphi>

Suporte SMP

A boa notícia é que se sua aplicação trabalhar com suporte a multi-tarefa desta forma, já terá SMP habilitado!

Depuração de aplicações multi-tarefa com Lázaro

A depuração no Lazarus não está ainda totalmente funcional.

Depuração de saída

Em um aplicativo de thread simples, você pode simplesmente escrever console/terminal/ou qualquer um e a ordem das linhas é a mesma que foram escritas. Em aplicações multi-tarefa as coisas são mais complicadas. Se duas threads estão escrevendo, dizem que uma linha é escrita pela thread A antes de uma thread B, as linhas não são necessariamente escritas nessa ordem. Pode acontecer, de uma thread escrever sua saída, enquanto outra thread está escrevendo uma linha.

A unidade LCLProc contem várias funções, para deixar cada thread escrever o seu próprio arquivo log.

<delphi>

procedure DbgOutThreadLog(const Msg: string); overload;
procedure DebuglnThreadLog(const Msg: string); overload;
procedure DebuglnThreadLog(Args: array of const); overload;
procedure DebuglnThreadLog; overload;

</delphi>

Por Exemplo: Em vez de writeln('Algum texto ',123); use DebuglnThreadLog(['Some text ',123]); Isto irá adicionar uma linha 'Algum texto 123' em Log<PID>.txt, onde <PID> e o ID do processo da thread corrente.

É uma boa idéia remover o arquivo de log antes de cada execução:

rm -f Log* && ./project1

Linux

Se você tentar depurar um aplicativo multi-tarefa em Linux, você terá um grande problema: o servidor X irá travar. Não se sabe como resolver isso corretamente, mas uma solução é:

Crie uma nova instância do X:

X :1 &

Será aberta uma nova sessão X, e quando você mudar para a nova sessão X (para retornar ao que você estava trabalhando pressione CTRL+ALT+F7), você será capaz de voltar para a nova tela gráfica usando CTRL+ALT+F8 (Se essa combinação não funcionar, tente com CTRL+ALT+F2 ...isto se estiver trabalhado com Slackware)

Então você poderia, se quiser, criar uma área de trabalho na sessão X iniciada com:

gnome-session --display=:1 &

Então, no Lazaro, nos parâmetros de execução do projeto, verifique “Use display” e digite :1. Agora a aplicação será executada no segundo servidor X e depurada no primeiro. Isto foi testado com Free Pascal 2.0 e Lazarus 0.9.10 no Windows e Linux.


Em vez de criar uma nova sessão X, pode-se usar Xnest. Xnest é uma sessão X em uma janela. Usando isto o servidor X não travará durante a depuração das threads, e é muito mais fácil de depurar sem ter que alternar entre os terminais o tempo todo.

A linha de comando para executar Xnest é

Xnest :1 -ac

para criar uma sessão X sobre :1, e desabilitar os controles de acesso.