Multithreaded Application Tutorial/pt

From Free Pascal wiki
Jump to navigationJump to search

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 quer somente 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 que 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:

 
  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;

Na aplicação:

  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;

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:

 
  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;

Na Aplicação,

  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;

Coisas para ter cuidados 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 é:

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

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 unidade do programa, .lpr)! Assim, o código do aplicativo Lazaro deve ser parecido:

 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 },

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:

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

Suporte para Sistema de Multi Processo Simétrico (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 Lazaro

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.

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

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 seção X, e quando você mudar para a nova seçã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 seçã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 seção X, pode-se usar Xnest. Xnest é uma seçã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 seção X sobre :1, e desabilitar os controles de acesso.

Widgetsets

As interfaces win32, GTK e a carbono suportam plenamente multi-tarefa. Isso significa que a classe TThreads, as seções críticas (TCriticalSection) e a sincronização (Synchronize) funcionarão.

Seções críticas

Uma seção crítica é um objeto usado para certificar-se, de que alguma parte do código será executado apenas por uma thread de cada vez. Uma seção crítica precisa ser criada e inicializada antes de ser usada e deve ser liberado quando não for mais necessário.

As seções críticas são normalmente utilizados da seguinte maneira:

Adicione a unidade SyncObjs.

Declare a seção globalmente para todos as threads que devem ter acesso:

 MyCriticalSection: TRTLCriticalSection;

Crie a seção:

 InitializeCriticalSection(MyCriticalSection);

Execute alguma thread, fazendo algo que deve ser exclusivo

 EnterCriticalSection(MyCriticalSection);
 try
   // access some variables, write files, send some network packets, etc
 finally
   LeaveCriticalSection(MyCriticalSection);
 end;

Após ter terminado todas as threads, libere a seção crítica:

 DeleteCriticalSection(MyCriticalSection);

Como alternativa, você pode usar um objeto TcriticalSection. A criação é feita pelo Inicialization, o método Enter é feita pelo EnterCriticalSection, o método Leave é feita pelo LeaveCriticalSection e a destruição do objeto é feita pelo deletion.

Por exemplo: 5 threads incrementando um contador. Veja lazarus/examples/multithreading/criticalsectionexample1.lpi

CUIDADO: Há dois conjuntos das 4 funções acima. Um Conjunto da RTL e outra da LCL. As funções da LCL estão definida nas unidade LCLIntf e LCLType. Ambos trabalham de forma parecida. Você pode usar os dois ao mesmo tempo na sua aplicação, mas você não deve usar uma função RTL com uma seção crítica LCL, e vice versa.

Compartilhamento de variáveis

Se algumas threads compartilharem uma variável, que é somente leitura, então não tem nada com que se preocupar. Mas se uma ou diversas threads escrevem na variável, então você deve se certificar de que somente uma thread por vez acessará a variável.

Por exemplo: 5 threads incrementando um contador: veja lazarus/examples/multithreading/criticalsectionexample1.lpi

Esperando por outra thread

Se a thread A precisar de um resultado de outra thread B, ela deve esperar, até que B tenha terminado.

Importante: A thread principal nunca deve esperar por outra thread. Ao invez disso use Synchronize (veja acima).

Veja um exemplo: lazarus/examples/multithreading/waitforexample1.lpi

 
{ TThreadA }
 
procedure TThreadA.Execute;
begin
  Form1.ThreadB:=TThreadB.Create(false);
  // create event
  WaitForB:=RTLEventCreate;
  while not Application.Terminated do begin
    // wait infinitely (until B wakes A)
    RtlEventWaitFor(WaitForB);
    writeln('A: ThreadB.Counter='+IntToStr(Form1.ThreadB.Counter));
  end;
end;
 
{ TThreadB }
 
procedure TThreadB.Execute;
var
  i: Integer;
begin
  Counter:=0;
  while not Application.Terminated do begin
    // B: Working ...
    Sleep(1500);
    inc(Counter);
    // wake A
    RtlEventSetEvent(Form1.ThreadA.WaitForB);
  end;
end;

Nota: RtlEventSetEvent pode ser chamado antes de RtlEventWaitFor. Então RtlEventWaitFor retornará imediatamente. Use RTLeventResetEvent para limpar um indicador.

Fork

Quando replicamos uma aplicação mult-threaded, esteja ciente de que qualquer thread criada e executada ANTES de realizar a replica, NÃO será executado no processo filho. Como indicado no manual do fork(). Todas as threads que estavam sendo executadas antes da chamada da replica, seu estado será indefinido.

Portanto, esteja ciente de qualquer thread inicializada antes da chamada (incluindo na seção de inicialização). Eles não irão funcionar.

Processos paralelos/laços

Um caso especial de multi-tarefa e executando um procedimento único em paralelo. veja Parallel procedures.

Computação distribuída

Os próximos maiores passos, após programas multi-tarefa, é executar os segmentos em várias máquinas.

  • Você pode usar uma das suítes do TCP como sinapse, LNET ou Indy para comunicações. Isto lhe dá a máxima flexibilidade e é largamente utilizado para aplicações cliente / servidor com conexões rápidas.
  • Você pode usar bibliotecas de envio de mensagens, como MPICH, que são utilizados para HPC (High Performance Computing) em clusters.