Difference between revisions of "Multithreaded Application Tutorial/es"

From Lazarus wiki
Jump to navigationJump to search
Line 391: Line 391:
 
  end;</delphi>
 
  end;</delphi>
  
== Fork ==
+
== Réplica (''Fork'') ==
  
&nbsp;&nbsp;&nbsp;When forking in a multithreaded 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.
+
&nbsp;&nbsp;&nbsp;Cuándo replicamos una aplicación con múltiples hilos, hay que tener presente que los hilos han se ser creados y estar en funcionamiento '''antes''' de realizar la replicación (fork o fpFork, ya que '''no''' pueden ejecutarse en el proceso hijo. Como se indica en el manual de ''fork()'', el estado de los los hilos que se ejecutan antes de la bifurcación será indefinido.
  
&nbsp;&nbsp;&nbsp;So be aware of any threads initializing before the call (including on the initialization section). They will NOT work.
+
&nbsp;&nbsp;&nbsp;Por lo tanto, hay que tener en cuenta que cualquier hilo iniciado antes de la llamada (incluidos en la sección de inicio). No van a funcionar.
  
 
== Distributed computing ==
 
== Distributed computing ==

Revision as of 01:05, 30 July 2008

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

Descripción general

   En esta página se tratará de explicar cómo escribir y depurar una aplicación multihilo con Free Pascal y Lazarus.

   Una aplicación multihilo es aquella que crea dos o más hilos de ejecución que trabajan al mismo tiempo.

   Si eres nuevo en programación multihilo, por favor lee el apartado "¿Necesito multihilo?" para saberlo realmente. Puedes ahorrarte un montón de dolores de cabeza.

   Uno de los hilos será el Hilo Principal, este es el que crea el Sistema Operativo al arrancar la aplicación.

   El Hilo Principal debe ser el único hilo que actualice los componentes de la interfaz con el usuario, en caso contrario la aplicación dejará de funcionar.

   La idea principal es que la aplicación puede hacer alguna tarea en el fondo (en un segundo hilo), mientras que el usuario puede seguir trabajando (con el hilo principal).

   Otro uso de los hilos es para tener una aplicación que responda mejor. Si crea una aplicación, y cuando el usuario pulsa un botón, la aplicación inicia un proceso (un gran trabajo) ... y durante el proceso, la pantalla deja de responder, el usuario tendrá la sensación de que la aplicación está muerta, lo cual no es agradable. Si la tarea pesada se ejecuta en un segundo hilo, la aplicación puede seguir respondiendo (o casi) como si estuviera desocupada. En este caso es una buena idea, antes de empezar el hilo, desactivar los botones de la ventana para evitar que el usuario inicie de más de un hilo para ese trabajo.

   Otro uso más, es crear un servidor capaza de responder a muchos clientes al mismo tiempo.

¿Necesitas múltiples hilos?

   Si eres novato en esto de los múltiples hilos y lo único que deseas es hacer que tu aplicación responda antes, mientras realiza un trabajo muy pesado, es posible que esto no sea lo que buscas.

   Las aplicaciones con múltiples hilos de ejecución son siempre más difíciles de depurar y resultan más complejas. Y muchas veces es innecesario, con un solo hilo es suficiente. Se puede dividir la tarea pesada en varios bloques más pequeños, y usar en el lugar adecuado Application.ProcessMessages, que permite a la LCL gestionar los mensajes en espera y continuar después con nuestro código. La idea es realizar una parte del trabajo, llamar a Application.ProcessMessages para comprobar si el usuario hace clic en Cancelar o en algún otro sitio o si es necesario repintar la ventana, y luego continuaremos con la siguiente parte del trabajo, llamaremos de nuevo a Application.ProcessMessages y así sucesivamente.

   Por ejemplo: Leer un fichero muy grande y procesarlo.

   Ver ejemplo en ${LazarusDir}/examples/multithreading/singlethreadingexample1.lpi.

   La programación con múltiples hilos es necesaria únicamente para

  • Bloqueo de enlaces, como en las comunicaciones de red
  • Utilizar múltiples procesadores a la vez
  • Llamadas a librerías y algoritmos que no pueden ser divididos en partes más pequeñas

The TThread Class

   The following example can be found in the examples/multithreading/ directory.

   To create a multithreaded application, the easiest way is to use the TThread Class.

   This class permits the creation of an additional thread (alongside the main thread) in a simple way.

   Normally you only have to override 2 methods: the Create constructor, and the Execute method.

   In the constructor, you will prepare the thread to run. You will set the initial values of the variables or properties you need. The original constructor of TThread requires a parameter called Suspended. As you might expect, setting Suspended = True will prevent the thread starting automatically after the creation. If Suspended = False, the thread will start running just after the creation. If the thread is created suspended, then it will run only after the Resume method is called.

   As of FPC version 2.0.1 and later, TThread.Create also has an implicit parameter for Stack Size. You can now change the default stack size of each thread you create if you need it. Deep procedure call recursions in a thread are a good example. If you don't specify the stack size parameter, a default OS stack size is used.

   In the overrided Execute method you will write the code that will run on the thread.

   The TThread class has one important property:<delphi>Terminated : boolean;</delphi>

   If the thread has a loop (and this is usual), the loop should be exited when Terminated is true (it is false by default). So in each cycle, it must check if Terminated is True, and if it is, must exit the .Execute method as quickly as possible, after any necessary cleanup.

   So keep in mind that the Terminate method does not do anything by default: the .Execute method must explicitly implement support for it to quit it's job.

   As we explained earlier, the thread should not interact with the visible components. To show something to the user it must do so in the main thread. To do this, a TThread method called Synchronize exists. Synchronize requires a method (that takes no parameters) as an argument. When you call that method through Synchronize(@MyMethod), the thread execution will be paused, the code of MyMethod will run in the main thread, and then the thread execution will be resumed. The exact working of Synchronize depends on the platform, but basically it does this: It posts a message onto the main message queue and goes to sleep. Eventually the main thread processes the message and calls MyMethod. This way MyMethod is called without context, that means not during a mouse down event or during paint event, but after. After the main thread executed MyMethod, it wakes the sleeping Thread and processes the next message. The Thread then continues.

   There is another important property of TThread: FreeOnTerminate. If this property is true, the thread object is automatically freed when the thread execution (.Execute method) stops. Otherwise the application will need to free it manually.

   Example:

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

   On the application,

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

   If you want to make your application more flexible you can create an event for the thread - this way your synchronized method won't be tightly coupled with a specific form or class - you can attach listeners to the thread's event. Here is an example:

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

   On the application,

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

Cosas con las que tener especial cuidado

    En Windows hay un posible problema cuándo se utilizan hilos conjuntamente con el modificador -Ct (comprobar pila). Por razones no muy claras la comprobación de la pila se dispara en con todos los TThread.Create si se utiliza el tamaño de pila por defecto. La única solución, por el momento, es simplemente no utilizar el modificador -Ct. Tenga en cuenta que no se producirá una excepción en el hilo principal, pero sí en el hilo recién creado. Es como si el hilo nunca se hubiera iniciado.

   Un código adecuado para comprobar si esta u otras excepciones pueden ocurrir en la creación del hilo es:

<delphi> MyThread:=TThread.Create(False);

        if Assigned(MyThread.FatalException) then
           raise MyThread.FatalException;</delphi>

   Este código asegura que cualquier excepción que se produzca durante la creación de un hilo será lanzada en su hilo principal.

Units needed for a multithreaded application

   You don´t need any special unit for this to work with Windows. However with Linux, MacOSX and FreeBSD, you need the cthreads unit and it must be the first used unit of the project (the program unit, .lpr)!

   So, your Lazarus application code should look like:

<delphi> program MyMultiThreadedProgram;

 {$mode objfpc}{$H+}
 uses
 {$ifdef unix}
   cthreads,
 {$endif}
   Interfaces, // this includes the LCL widgetset
   Forms
   { add your units here },</delphi>

   If you forget this you will get this error on startup:

 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.

   Packages which uses multithreading should add the -dUseCThreads flag to the custom usage options. Open the package editor of the package, then Options > Usage > Custom and add -dUseCThreads. This will define this flag to all projects and packages using this package, including the IDE. The IDE and all new applications created by the IDE have already the following code in their .lpr file: <delphi> uses

   {$IFDEF UNIX}{$IFDEF UseCThreads}
   cthreads,
   {$ENDIF}{$ENDIF}</delphi>

Soporte para Sistema de Multiproceso Simétrico (SMP)

   Las buenas noticias es que si tu aplicación funciona correctamente con múltiples hilos de esta forma, está también preparado para SMP.

Debuging Multithreaded Applications with Lazarus

   The debugging on Lazarus is not fully functional yet.

Debugging output

   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 multithreaded 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 neccessarily written in that order. It can even happen, that a thread writes its output, while the other thread is writing a line.

   The LCLProc unit contains several functions, to let each thread write to its own log file: <delphi> procedure DbgOutThreadLog(const Msg: string); overload;

 procedure DebuglnThreadLog(const Msg: string); overload;
 procedure DebuglnThreadLog(Args: array of const); overload;
 procedure DebuglnThreadLog; overload;</delphi>

   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 multithreaded application on Linux, you will have one big problem: the X server will hang.

   It is unknown how to solve this properly, but a workaround is:

   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 seccond 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.

Artefactos Widgetsets

   Las interfaces de win32, la de gtk y la de carbon soportan completamente la programación con múltiples hilos. Esto significa, que la clase TThread, las secciones críticas (TCriticalSection) y la sincronización (Synchronize) funcionarán.

Critical sections

   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:

   Add the unit SyncObjs.

   Declare the section (globally for all threads which should access the section): <delphi> MyCriticalSection: TRTLCriticalSection;</delphi>

   Create the section: <delphi> InitializeCriticalSection(MyCriticalSection);</delphi>

   Run some threads. Doing something exclusively <delphi> EnterCriticalSection(MyCriticalSection);

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

   After all threads terminated, free it: <delphi> DeleteCriticalSection(MyCriticalSection);</delphi>

   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.

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

   BEWARE: There are two sets of the above 4 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 with an LCL Critical Section and vice versus.

Sharing Variables

   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

Waiting for another thread

   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

<delphi> { 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;</delphi>

Réplica (Fork)

   Cuándo replicamos una aplicación con múltiples hilos, hay que tener presente que los hilos han se ser creados y estar en funcionamiento antes de realizar la replicación (fork o fpFork, ya que no pueden ejecutarse en el proceso hijo. Como se indica en el manual de fork(), el estado de los los hilos que se ejecutan antes de la bifurcación será indefinido.

   Por lo tanto, hay que tener en cuenta que cualquier hilo iniciado antes de la llamada (incluidos en la sección de inicio). No van a funcionar.

Distributed computing

   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.