Multithreaded Application Tutorial/pl

From Lazarus wiki

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

Kurs tworzenia aplikacji wielowątkowych


Wprowadzenie

Na tej stronie spróbujemy wyjaśnić, w jaki sposób napisać i debugować aplikacje wielowątkowe przy pomocy Free Pascala i Lazarusa. Aplikacja wielowątkowa to program, który tworzy dwa lub więcej wątków wykonawczych działających w tym samym czasie. Jeśli nie miałeś dotąd do czynienia z wielowątkowością, przeczytaj akapit „Czy potrzebujesz wielowątkowości?” aby ustalić, czy jest ci to naprawdę potrzebne, bo być może zaoszczędzisz sobie bólu głowy.

Pierwszy wątek nazywany jest głównym wątkiem. Główny wątek to ten, który jest tworzony przez System Operacyjny, to ten sam, w którym nasza aplikacja rozpoczyna działanie. Główny wątek musi być jedynym wątkiem, który aktualizuje komponenty do komunikacji z użytkownikiem: w przeciwnym wypadku, aplikacja może się zawiesić.

Podstawowym założeniem jest to, aby aplikacja mogła przetwarzać pewne dane w tle, tj. w drugim wątku, podczas gdy użytkownik może kontynuować pracę przy użyciu głównego wątku.

Innym zastosowaniem wątków jest po prostu możliwość lepszej reakcji programu. Jeśli tworzysz dużą aplikację lub gdy użytkownik naciśnie przycisk aplikacji rozpoczynając jakiś duży proces ... dopóki trwa przetwarzanie, ekran przestaje odpowiadać, co powoduje błędne lub mylące wrażenie, że aplikacja jest zawieszona. Jeśli duży proces przebiega w drugim wątku, aplikacja zachowuje się (prawie) tak, jakby była w stanie bezczynności. W tym przypadku dobrym pomysłem jest aby, przed rozpoczęciem wątku, wyłączyć odpowiednie przyciski na formularzu, w celu uniknięcia ponownego uruchomienia drugiego wątku przez użytkownika.

Jeszcze innym zastosowaniem wielowątkowości może być serwer, który jest w stanie dać odpowiedź wielu klientom, w tym samym czasie.

Czy potrzebujesz wielowątkowości?

Jeśli dopiero poznajesz wielowątkowość i chciałbyś tylko stworzyć aplikację z szybszym czasem reakcji w chwili gdy wykonuje ona umiarkowanie długotrwałe zadanie, wówczas wielowątkowość może być nadmiarowa w stosunku do wymagań. Aplikacje wielowątkowe są zawsze trudniejsze do debugowania i często są znacznie bardziej skomplikowane, jednak w wielu przypadkach wcale nie trzebujesz używać wielowątkowości. Jeden wątek jest wystarczający. Możesz podzielić czasochłonne zadanie na kilka mniejszych części, oraz użyć procedurę Application.ProcessMessages. Procedura ta pozwala bibliotece LCL obsłużyć wszystkie oczekujące komunikaty i powrócić do miejsca jej wywołania. Główną ideą jest to, aby wywoływać Application.ProcessMessages w regularnych odstępach czasu w trakcie wykonywania długotrwałego zadania, np. po to aby sprawdzić, czy użytkownik kliknął na jakąś kontrolkę lub czy wskaźnik postępu musi zostać przemalowany albo zdarzyło się jeszcze coś innego.

Przykład: Czytanie dużego pliku i jego przetwarzanie. Zobacz: examples/multithreading/singlethreadingexample1.lpi.

Wielowątkowość jest potrzebna tylko w przypadku

  • używania uchwytów blokujących, takich jak w komunikacji sieci
  • korzystania z wielu procesorów jednocześnie (SMP)
  • algorytmów i bibliotek, które muszą być wywoływane przez API i jako takie nie mogą być podzielone na mniejsze części.

Jeśli chcesz użyć wielowątkowości, aby zwiększyć prędkość przy użyciu wielu procesorów jednocześnie, sprawdź, czy twój obecny program w tej chwili wykorzystuje 100% zasobów 1 rdzenia CPU (na przykład, program może aktywnie korzystać z operacji wejścia-wyjścia, jak zapis do pliku i to zajmuje dużo czasu, ale nie obciąża procesora, i w tym przypadku program nie będzie działał szybciej z wieloma wątkami). Należy również sprawdzić, czy poziom optymalizacji jest ustawiony na maksymalny (3). Przełączając poziom optymalizacji z 1 na 3, program może stać się około 5 razy szybszy.

Moduły potrzebne do tworzenia aplikacji wielowątkowej

Nie potrzebujesz żadnych specjalnych modułów do tego, aby pracować z systemem Windows. Jednak w przypadku Linuksa, Mac OS X i FreeBSD, należy użyć modułu cthreads i musi być on użyty jako pierwszy moduł projektu (program źródłowy, zwykle znajdujący się w pliku .lpr)!

Dlatego twój kod aplikacji Lazarusa powinien wyglądać tak:

program MyMultiThreadedProgram;
{$mode objfpc}{$H+}
uses
{$ifdef unix}
  cthreads,
  cmem, // menedżer pamięci c jest w niektórych systemach znacznie szybszy dla aplikacji wielowątkowych
{$endif}
  Interfaces, // to zawiera widżety LCL
  Forms
  { tu możesz dodać następne moduły },

Jeśli o tym zapomnisz i użyjesz TThread, otrzymasz następujący błąd podczas uruchamiania:

 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.
Note-icon.png

Uwaga: Jeśli pojawi się błąd linkera o tym, że nie znaleziono „mcount” to znaczy, że używasz jakiegoś modułu, który zawiera kod wielowątkowy i musisz dodać moduł cthreads lub użyć sprytnego łączenia.

Note-icon.png

Uwaga: Jeśli w procedurze SYSTEM_NOTHREADERROR otrzymasz błąd: „Project raised exception class 'RunError(232)'” to znaczy, że twój kod potrzebuje wątków i musisz dodać moduł cthreads.

Przykład w czystym FPC

Poniższy kod przedstawia bardzo prosty przykład. Testowany z FPC 3.0.4 na Win7.

Program ThreadTest;
{test multi threading capability }
{
      OUTPUT
thread 1 started
thread 1 thri 0 Len(S)= 1
thread 1 thri 1 Len(S)= 2
thread 1 thri 2 Len(S)= 3
thread 1 thri 3 Len(S)= 4
thread 1 thri 4 Len(S)= 5
thread 1 thri 5 Len(S)= 6
thread 1 thri 6 Len(S)= 7
thread 1 thri 7 Len(S)= 8
thread 1 thri 8 Len(S)= 9
thread 1 thri 9 Len(S)= 10
thread 1 thri 10 Len(S)= 11
thread 1 thri 11 Len(S)= 12
thread 1 thri 12 Len(S)= 13
thread 1 thri 13 Len(S)= 14
thread 1 thri 14 Len(S)= 15
thread 2 started
thread 3 started
thread 1 thri 15 Len(S)= 16
thread 2 thri 0 Len(S)= 1
thread 3 thri 0 Len(S)= 1
thread 1 thri 16 Len(S)= 17
...
...
thread 5 thri 997 Len(S)= 998
thread 5 thri 998 Len(S)= 999
thread 5 thri 999 Len(S)= 1000
thread 5 finished
thread 10 thri 828 Len(S)= 829
thread 9 thri 675 Len(S)= 676
thread 4 thri 656 Len(S)= 657
thread 10 thri 829 Len(S)= 830
thread 9 thri 676 Len(S)= 677
thread 9 thri 677 Len(S)= 678
thread 10 thri 830 Len(S)= 831
thread 10 thri 831 Len(S)= 832
thread 10 thri 832 Len(S)= 833
thread 10 thri 833 Len(S)= 834
thread 10 thri 834 Len(S)= 835
thread 10 thri 835 Len(S)= 836
thread 10 thri 836 Len(S)= 837
thread 10 thri 837 Len(S)= 838
thread 10 thri 838 Len(S)= 839
thread 10 thri 839 Len(S)= 840
thread 9 thri 678 Len(S)= 679
...
...
thread 4 thri 994 Len(S)= 995
thread 4 thri 995 Len(S)= 996
thread 4 thri 996 Len(S)= 997
thread 4 thri 997 Len(S)= 998
thread 4 thri 998 Len(S)= 999
thread 4 thri 999 Len(S)= 1000
thread 4 finished
10
	  
}

uses
  {$ifdef unix}cthreads, {$endif} sysutils;

const
  threadcount = 10;
  stringlen = 1000;

var
   finished : longint;

threadvar
   thri : ptrint;

function f(p : pointer) : ptrint;
var
  s : ansistring;
begin
  Writeln('thread ',longint(p),' started');
  thri:=0;
  while (thri<stringlen) do begin
    s:=s+'1'; { create a delay }
    writeln('thread ',longint(p),' thri ',thri,' Len(S)= ',length(s));
	inc(thri);
  end;
  Writeln('thread ',longint(p),' finished');
  InterLockedIncrement(finished);
  f:=0;
end;


var
   i : longint;

Begin
   finished:=0;
   for i:=1 to threadcount do
     BeginThread(@f,pointer(i));
   while finished<threadcount do ;
   Writeln(finished);
End.

Klasa TThread

Poniższy przykład można znaleźć w katalogu examples/multithreading/.

Aby utworzyć aplikację wielowątkową, najłatwiej jest użyć klasy TThread. Ta klasa pozwala w prosty sposób stworzyć dodatkowy wątek (obok głównego wątku). Zwykle wymagane jest przesłonięcie tylko dwóch metod: konstruktora Create i metody Execute.

W konstruktorze przygotujesz wątek do uruchomienia. Ustawisz początkowe wartości dla zmiennych lub właściwości, których potrzebujesz. Oryginalny konstruktor TThread wymaga parametru o nazwie Suspended. Jak można się spodziewać, ustawienie Suspended = True zapobiegnie automatycznemu uruchomieniu wątku po utworzeniu. Jeśli Suspended = False, wątek zacznie działać zaraz po utworzeniu. Jeśli wątek zostanie utworzony jako zawieszony, zostanie uruchomiony dopiero po wywołaniu metody Start.

Note-icon.png

Note: Metoda Resume jest przestarzała od FPC 2.4.4. Została zastąpiona przez metodę Start.

Począwszy od wersji FPC 2.0.1 i nowszych, TThread.Create ma również niejawny parametr dla rozmiaru stosu. W razie potrzeby możesz teraz zmienić domyślny rozmiar stosu każdego utworzonego wątku. Dobrym przykładem są głębokie rekurencje wywołań procedur w wątku. Jeśli nie określisz parametru rozmiaru stosu, zostanie użyty domyślny rozmiar stosu systemu operacyjnego.

W zastąpionej metodzie Execute napisz kod, który będzie uruchamiany w wątku.

Klasa TThread ma jedną ważną właściwość: Terminated : boolean;

Jeśli wątek ma pętlę (i jest to typowe), pętla powinna zostać zakończona, gdy Terminated ma wartość true (domyślnie jest to false). W każdym przebiegu należy sprawdzić wartość Terminated, a jeśli jest prawdziwa, pętla powinna zostać zakończona tak szybko, jak to konieczne, po każdym koniecznym czyszczeniu. Należy pamiętać, że metoda Terminate domyślnie nic nie robi: metoda .Execute musi jawnie zaimplementować obsługę, aby zakończyć swoje zadanie.

Jak wyjaśniliśmy wcześniej, wątek nie powinien wchodzić w interakcje z widocznymi komponentami. Aktualizacje widocznych komponentów muszą być dokonywane w kontekście głównego wątku.

Aby to zrobić, istnieje metoda TThread o nazwie Synchronize. Synchronize wymaga metody w wątku (która nie przyjmuje parametrów) jako argumentu. Po wywołaniu tej metody za pomocą Synchronize(@MyMethod) wykonanie wątku zostanie wstrzymane, kod MyMethod zostanie wywołany z głównego wątku, a następnie zostanie wznowione wykonywanie wątku.

Dokładne działanie metody Synchronize zależy od platformy, ale zasadniczo wykonuje ona:

  • wysyła wiadomość do głównej kolejki wiadomości i zasypia
  • ostatecznie główny wątek przetwarza wiadomość i wywołuje MyMethod. W ten sposób MyMethod jest wywoływana bez kontekstu, co oznacza, że ​​nie wykonuje się podczas zdarzenia wciśnięcia myszy lub podczas malowania, ale po.
  • po wykonaniu przez główny wątek MyMethod, budzi ona uśpiony wątek i przetwarza następną wiadomość
  • następnie wątek jest kontynuowany.

Jest jeszcze jedna ważna właściwość TThread: FreeOnTerminate. Jeśli ta właściwość ma wartość true, obiekt wątku jest automatycznie zwalniany po zatrzymaniu wykonywania wątku (metoda .Execute). W przeciwnym razie aplikacja będzie musiała zwolnić ją ręcznie.

Przykład:

  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
    inherited Create(CreateSuspended);
    FreeOnTerminate := True;
  end;

  procedure TMyThread.ShowStatus;
  // ta metoda jest wykonywana przez główny wątek i dlatego może uzyskać dostęp do wszystkich elementów GUI.
  begin
    Form1.Caption := fStatusText;
  end;
 
  procedure TMyThread.Execute;
  var
    newStatus : string;
  begin
    fStatusText := 'TMyThread Starting...';
    Synchronize(@Showstatus);
    fStatusText := 'TMyThread Running...';
    while (not Terminated) and ([inne wymagane warunki]) do
      begin
        ...
        [tutaj jest kod głównej pętli wątku]
        ...
        if NewStatus <> fStatusText then
          begin
            fStatusText := newStatus;
            Synchronize(@Showstatus);
          end;
      end;
  end;

W aplikacji:

  var
    MyThread : TMyThread;
  begin
    MyThread := TMyThread.Create(True); // W ten sposób nie uruchamia się automatycznie
    ...
    [Tutaj kod inicjuje wszystko, co jest wymagane, zanim wątki zaczną się wykonywać]
    ...
    MyThread.Start;
  end;

Jeśli chcesz, aby Twoja aplikacja była bardziej elastyczna, możesz utworzyć zdarzenie dla wątku; w ten sposób Twoja zsynchronizowana metoda nie będzie ściśle powiązana z określoną formą lub klasą: możesz dołączyć detektory do zdarzenia wątku. Oto przykład:

  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
    inherited Create(CreateSuspended);
    FreeOnTerminate := True;
  end;

  procedure TMyThread.ShowStatus;
  // ta metoda jest wykonywana przez główny wątek i dlatego może uzyskać dostęp do wszystkich elementów GUI.
  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 ([inne wymagane warunki]) do
      begin
        ...
        [tutaj jest kod głównej pętli wątku]
        ...
        if NewStatus <> fStatusText then
          begin
            fStatusText := newStatus;
            Synchronize(@Showstatus);
          end;
      end;
  end;

W aplikacji:

  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;

    // FreeOnTerminate ma wartość true więc nie powinniśmy pisać:
    // MyThread.Free;
    inherited;
  end;

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

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

Ważne rzeczy, na które trzeba zwrócić uwagę

Kontrola stosu w systemie Windows

Może wystąpić potencjalny problem w systemie Windows, jeśli używasz przełącznika -Ct (sprawdzanie stosu). Z powodów, które nie są do końca jasne, sprawdzanie stosu będzie „wyzwalane” z każdym wywołaniem TThread.Create, jeśli używasz domyślnego rozmiaru stosu. W tej chwili jedynym rozwiązaniem jest po prostu nieużywanie przełącznika -Ct. Zauważ, że NIE powoduje to wyjątku w głównym wątku, ale w nowo utworzonym. To „wygląda” tak, jakby wątek nigdy nie został uruchomiony.

Poprawny kod do sprawdzenia tego i innych wyjątków, które mogą wystąpić podczas tworzenia wątków, to:

MyThread := TThread.Create(False);

if Assigned(MyThread.FatalException) then
  raise MyThread.FatalException;

Ten kod zapewni, że każdy wyjątek, który wystąpił podczas tworzenia wątku, zostanie zgłoszony w głównym wątku.

Wielowątkowość w pakietach

Pakiety korzystające z wielowątkowości powinny dodawać flagę -dUseCThreads do dodatkowo używanych opcji. Otwórz edytor pakietów, a następnie Opcje > Użycie > Dodatkowe i dodaj -dUseCThreads. Spowoduje to zdefiniowanie tej flagi dla wszystkich projektów i pakietów korzystających z tego pakietu, w tym IDE. IDE i wszystkie nowe aplikacje utworzone przez IDE mają już następujący kod w swoim pliku .lpr:

uses
  {$IFDEF UNIX}{$IFDEF UseCThreads}
  cthreads,
  cmem, // Menedżer pamięci c jest w niektórych systemach znacznie szybszy w przypadku wielowątkowości
  {$ENDIF}{$ENDIF}

Moduł Heaptrc

Nie można używać przełącznika -gh z modułem cmem. Przełącznik -gh używa modułu heaptrc, który rozszerza menedżer stosu. Dlatego moduł heaptrc musi być używany after module cmem.

uses
  {$IFDEF UNIX}{$IFDEF UseCThreads}
  cthreads,
  cmem, // Menedżer pamięci c jest w niektórych systemach znacznie szybszy w przypadku wielowątkowości
  {$ENDIF}{$ENDIF}
  heaptrc,

Initialization and Finalization

Aby zainicjować sam obiekt wątku, możesz uruchomić go w trybie zawieszenia i ustawić jego właściwości i/lub utworzyć nowy konstruktor i wywołać odziedziczony konstruktor.

Uwaga: Używanie AfterConstruction, gdy CreateSuspended=false jest niebezpieczne, ponieważ wątek już zaczął działać.

Z drugiej strony, destruktor może służyć do finalizowania zasobów obiektu.

type
  TMyThread = class(TThread)
  private
    fRTLEvent: PRTLEvent;
  public
    procedure Create(SomeData: TSomeObject); override;
    destructor Destroy; override;
  end;

procedure TMyThread.Create(SomeData: TSomeObject; CreateSuspended: boolean);
begin
  // przykład: skonfiguruj zdarzenia, sekcje krytyczne i inne zasoby, takie jak pliki lub połączenia z bazami danych
  RTLEventCreate(fRTLEvent);
  inherited Create(CreateSuspended);
end;

destructor TMyThread.Destroy;
begin
  RTLeventDestroy(fRTLEvent);
  inherited Destroy;
end;

Program bez LCL

TThread.Synchronize wymaga, aby główny wątek regularnie wywoływał CheckSynchronize. LCL robi to w swojej pętli. Jeśli nie używasz pętli zdarzeń LCL, musisz ją wywołać samodzielnie.

Wsparcie dla SMP

The good news is that if your application works properly multi-threaded this way, it is already SMP enabled!

Debugowanie aplikacji wielowątkowych w Lazarusie

The debugging on Lazarus requires GDB and is rapidly becoming more and more fully featured and stable. However, there still exists a few Linux distributions with some problems.

Wyjście debuggera

In a single threaded application, you can simply write to console/terminal/whatever and the order of the lines is the same as they were written. In multi-threaded application things are more complicated. If two threads are writing, say a line is written by thread A before a line by thread B, then the lines are not necessarily written in that order. It can even happen, that a thread writes its output, while the other thread is writing a line.While under linux (maybe) you'll get proper DebugLn() output, under win32 you can get exceptions (probably DiskFull) because of DebugLn() usage outside of main thread.So, to avoid headaches use DebugLnThreadLog() mentioned below.

The LCLProc unit contains several functions, to let each thread write to its own log file:

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

For example: Instead of writeln('Some text ',123); use

 DebuglnThreadLog(['Some text ',123]);

This will append a line 'Some text 123' to Log<PID>.txt, where <PID> is the process ID of the current thread.

It is a good idea to remove the log files before each run:

 rm -f Log* && ./project1

Linux

If you try to debug a multi-threaded application on Linux, you will have one big problem: the Desktop Manager on X server can hang. This happens for instance when the application has captured the mouse/keyboard and was paused by gdb and the X server waits for your application. When that happens you can simply log in from another computer and kill the gdb or exit out of that session by pressing CTRL+ALT+F3 and kill gdb. Alternatively you can restart the window manager: enter sudo /etc/init.d/gdm restart. This will restart the desktop manager and get you back into your desktop.

Since it depends where gdb stops your program in some cases some tricks may help: for Ubuntu x64 set the Project options for debugging required extra information file...

 Project Options -> Compiler Options -> Linking -> Debugging: Check Use external gdb debug symbols file (-Xg).

The other option is to open anotner X desktop, run the IDE/gdb on one and the application on the other, so that only the test desktop freezes. Create a new instance of X with:

 X :1 &

It will open, and when you switch to another desktop (the one you are working with pressing CTRL+ALT+F7), you will be able to go back to the new graphical desktop with CTRL+ALT+F8 (if this combination does not work, try with CTRL+ALT+F2... this one worked on Slackware).

Then you could, if you want, create a desktop session on the X started with:

 gnome-session --display=:1 &

Then, in Lazarus, on the run parameters dialog for the project, check "Use display" and enter :1.

Now the application will run on the second X server and you will be able to debug it on the first one.

This was tested with Free Pascal 2.0 and Lazarus 0.9.10 on Windows and Linux.



Instead of creating a new X session, one can use Xnest. Xnest is a X session on a window. Using it X server didn't lock while debugging threads, and it's much easier to debug without keeping changing terminals.

The command line to run Xnest is

 Xnest :1 -ac

to create a X session on :1, and disabling access control.

Interfejsy widżetów Lazarusa

The win32, the gtk and the carbon interfaces support multi-threading. This means, TThread, critical sections and Synchronize work. But they are not thread safe. This means only one thread at a time can access the LCL. And since the main thread should never wait for another thread, it means only the main thread is allowed to access the LCL, which means anything that has to do with TControl, Application and LCL widget handles. There are some thread safe functions in the LCL. For example most of the functions in the FileUtil unit are thread safe.

Użycie SendMessage/PostMessage do komunikacji pomiędzy wątkami

Only one thread in an application should call LCL APIs, usually the main thread. Other threads can make use of the LCL through a number of indirect methods, one good option being the usage of SendMessage or PostMessage. LCLIntf.SendMessage and LCLIntf.PostMessage will post a message directed to a window in the message pool of the application.

See also the documentation for these routines:

The difference between SendMessage and PostMessage is the way that they return control to the calling thread. Like Synchronize, SendMessage blocks and control is not returned until the window that the message was sent to has completed processing it; however under certain circumstances SendMessage might attempt to optimise processing by remaining in the context of the thread that called it. With PostMessage control is returned immediately up to some system-defined maximum number of enqueued messages and as long as space remains on the heap for attached data.

In both cases the procedure handling the message (see below) should avoid calling application.ProcessMessages, since this might cause a second message to be dispatched which will be handled reentrantly. If this is unavoidable then it would probably be preferable to use some other mechanism to transfer serialised events between threads.

Here is an example of how a secondary thread could send text to be displayed in an LCL control to the main thread:

const
  WM_GOT_ERROR           = LM_USER + 2004;
  WM_VERBOSE             = LM_USER + 2005;

procedure VerboseLog(Msg: string);
var
  PError: PChar;
begin
  if MessageHandler = 0 then Exit;
  PError := StrAlloc(Length(Msg)+1);
  StrCopy(PError, PChar(Msg));
  PostMessage(formConsole.Handle, WM_VERBOSE, Integer(PError), 0);
end;

And an example of how to handle this message from a window:

const
  WM_GOT_ERROR           = LM_USER + 2004;
  WM_VERBOSE             = LM_USER + 2005;

type
  { TformConsole }

  TformConsole = class(TForm)
    DebugList: TListView;
    // ...
  private
    procedure HandleDebug(var Msg: TLMessage); message WM_VERBOSE;
  end;

var
  formConsole: TformConsole;

implementation

....

{ TformConsole }

procedure TformConsole.HandleDebug(var Msg: TLMessage);
var
  Item: TListItem;
  MsgStr: PChar;
  MsgPasStr: string;
begin
  MsgStr := PChar(Msg.wparam);
  MsgPasStr := StrPas(MsgStr);
  Item := DebugList.Items.Add;
  Item.Caption := TimeToStr(SysUtils.Now);
  Item.SubItems.Add(MsgPasStr);
  Item.MakeVisible(False);

// Followed by something like

  TrayControl.SetError(MsgPasStr);
  StrDispose(MsgStr)
end;

end.

Sekcje krytyczne

A critical section is an object used to make sure, that some part of the code is executed only by one thread at a time. A critical section needs to be created/initialized before it can be used and be freed when it is not needed anymore.

Critical sections are normally used this way:

Declare the section (globally for all threads which should access the section):

 MyCriticalSection: TRTLCriticalSection;

Create the section:

 InitializeCriticalSection(MyCriticalSection);

Run some threads. Doing something exclusively:

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

After all threads terminated, free it:

 DeleteCriticalSection(MyCriticalSection);

As an alternative, you can use a TCriticalSection object. The creation does the initialization, the Enter method does the EnterCriticalSection, the Leave method does the LeaveCriticalSection and the destruction of the object does the deletion.

Note that a critical section does not protect against the same thread entering the same block of code, only against different threads. For that reason it cannot be used to protect against e.g. reentry of a message handler (see the section above).

For example: 5 threads incrementing a counter. See lazarus/examples/multithreading/criticalsectionexample1.lpi

Warning-icon.png

Warning: There are two sets of the above four functions. The RTL and the LCL ones. The LCL ones are defined in the unit LCLIntf and LCLType. Both work pretty much the same. You can use both at the same time in your application, but you should not use a RTL function within an LCL Critical Section and vice versa.


Współdzielenie zmiennych

If some threads share a variable, that is read only, then there is nothing to worry about. Just read it. But if one or several threads changes the variable, then you must make sure, that only one thread accesses the variables at a time.

For example: 5 threads incrementing a counter. See lazarus/examples/multithreading/criticalsectionexample1.lpi

Oczekiwanie na inny wątek

If a thread A needs a result of another thread B, it must wait, till B has finished.

Important: The main thread should never wait for another thread. Instead use Synchronize (see above).

See for an example: 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;
Note-icon.png

Note: RtlEventSetEvent can be called before RtlEventWaitFor. Then RtlEventWaitFor will return immediately. Use RTLeventResetEvent to clear a flag.

Wątek potomny (fork)

When forking in a multi-threaded application, be aware that any threads created and running BEFORE the fork (or fpFork) call, will NOT be running in the child process. As stated on the fork() man page, any threads that were running before the fork call, their state will be undefined.

So be aware of any threads initializing before the call (including on the initialization section). They will NOT work.

Procedury/pętle równoległe

A special case of multi threading is running a single procedure in parallel. See Parallel procedures.

Obliczenia rozproszone

The next higher steps after multi threading is running the threads on multiple machines.

  • You can use one of the TCP suites like synapse, lnet or indy for communications. This gives you maximum flexibility and is mostly used for loosely connected Client / Server applications.
  • You can use message passing libraries like MPICH, which are used for HPC (High Performance Computing) on clusters.


Wątki zewnętrzne

To make Free Pascal's threading system work properly, each newly created FPC thread needs to be initialized (more exactly, the exception, I/O system and threadvar system per thread needs to be initialized so threadvars and heap are working). That is fully automatically done for you if you use BeginThread (or indirectly by using the TThread class). However, if you use threads that were created without BeginThread (i.e. external threads), additional work (currently) might be required. External threads also include those that were created in external C libraries (.DLL/.so).


Things to consider when using external threads (might not be needed in all or future compiler versions):

  • Do not use external threads at all - use FPC threads. If can you can get control over how the thread is created, create the thread by yourself by using BeginThread.

If the calling convention doesn't fit (e.g. if your original thread function needs cdecl calling convention but BeginThread needs pascal convention, create a record, store the original required thread function in it, and call that function in your pascal thread function:

type
 TCdeclThreadFunc = function (user_data:Pointer):Pointer;cdecl;

 PCdeclThreadFuncData = ^TCdeclThreadFuncData;
 TCdeclThreadFuncData = record
   Func: TCdeclThreadFunc;  //cdecl function
   Data: Pointer;           //original data
 end;

// The Pascal thread calls the cdecl function
function C2P_Translator(FuncData: pointer) : ptrint;
var
  ThreadData: TCdeclThreadFuncData;
begin
  ThreadData := PCdeclThreadFuncData(FuncData)^;
  Result := ptrint(ThreadData.Func(ThreadData.Data));
end;

procedure CreatePascalThread;
var
  ThreadData: PCdeclThreadFuncData;
begin
  New(ThreadData);
  // this is the desired cdecl thread function
  ThreadData^.Func := func;
  ThreadData^.Data := user_data;
  // this creates the Pascal thread
  BeginThread(@C2P_Translator, ThreadData );
end;


  • Initialize the FPC's threading system by creating a dummy thread. If you don't create any Pascal thread in your app, the thread system won't be initialized (and thus threadvars won't work and thus heap will not work correctly).
type
   tc = class(tthread)
     procedure execute;override;
   end;

   procedure tc.execute;
   begin
   end;

{ main program } 
begin
  { initialise threading system }
   with tc.create(false) do
   begin
     waitfor;
     free;
   end;
   { ... your code follows } 
end.

(After the threading system is initialized, the runtime may set the system variable "IsMultiThread" to true which is used by FPC routines to perform locks here and there. You should not set this variable manually.)


  • If for some reason this doesn't work for you, try this code in your external thread function:
function ExternalThread(param: Pointer): LongInt; stdcall;
var
  tm: TThreadManager;
begin
  GetThreadManager(tm);
  tm.AllocateThreadVars;
  InitThread(1000000); // adjust inital stack size here
  
  { do something threaded here ... }
    
  Result:=0;
end;


Identyfikacja zewnętrznych wątków

Sometimes you even don't know if you have to deal with external threads (e.g. if some C library makes a callback). This can help to analyse this:

1. Ask the OS for the ID of the current thread at your application's start

GetCurrentThreadID() //windows;
GetThreadID() //Darwin/macOS;  
TThreadID(pthread_self) //Linux;

2. Ask again for the ID of the current thread inside the thread function and compare this with the result of step 1.

Dodawanie opóźnień czasowych

ThreadSwitch()

Note-icon.png

Note: Nie używaj sztuczki z uśpieniem w Windows Sleep(0) ponieważ nie będzie to działać na wszystkich platformach.

Zobacz także