Difference between revisions of "Multithreaded Application Tutorial/es"

From Lazarus wiki
Jump to navigationJump to search
m (Fixed syntax highlighting; removed categories included in template)
 
(37 intermediate revisions by 3 users not shown)
Line 2: Line 2:
  
 
== Descripción general ==
 
== 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.
+
   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.
+
   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.
+
   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.
+
   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.
+
   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).
+
   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 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.
+
   Otro uso más, es crear un servidor capaz de responder a muchos clientes al mismo tiempo.
  
 
== ¿Necesitas múltiples hilos? ==
 
== ¿Necesitas múltiples hilos? ==
  
   Si eres novato en esto de los múltiples hilos y lo único que deseas es hacer tu aplicación responda antes, mientras realiza un trabajo muy pesado, es posible que esto no sea lo que buscas.
+
   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 no es necesario, con un solo hilo es suficiente. Si puedes dividir 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.
+
   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.
 
   Por ejemplo: Leer un fichero muy grande y procesarlo.
 
   
 
   
   Ver ejemplo en ${LazarusDir}/examples/multithreading singlethreadingexample1.lpi.
+
   Ver ejemplo en ${LazarusDir}/examples/multithreading/singlethreadingexample1.lpi.
  
   La programación con múltiples hilos se necesaria únicamente para
+
   La programación con múltiples hilos es necesaria únicamente para
 
* Bloqueo de enlaces, como en las comunicaciones de red
 
* Bloqueo de enlaces, como en las comunicaciones de red
 
* Utilizar múltiples procesadores a la vez
 
* Utilizar múltiples procesadores a la vez
 
* Llamadas a librerías y algoritmos que no pueden ser divididos en partes más pequeñas
 
* Llamadas a librerías y algoritmos que no pueden ser divididos en partes más pequeñas
  
== The TThread Class ==
+
== La clase TThread ==
  
The following example can be found in the examples/multithreading/ directory.
+
   El ejemplo que utilizaremos se puede encontrar en el directorio ${LazarusDir}/examples/multithreading/.
  
To create a multithreaded application, the easiest way is to use the TThread Class.
+
   La manera más fácil de crear una aplicación de múltiples hilos es utilizar la clase '''TThread'''. Esta clase permite la creación de un hilo adicional, junto con el hilo principal, de una manera sencilla.
  
This class permits the creation of an additional thread (alongside the main thread) in a simple way.
+
   Por lo general, sólo tienen que sobrescribir dos métodos: el constructor ''Create'' y el método ''Execute''.
  
Normally you only have to override 2 methods: the Create constructor, and the Execute method.
+
   En el constructor hay que preparar el hilo para su ejecución. Hay que dar el valor inicial a las variables y propiedades que lo precisen. El constructor original de TThread requiere un parámetro llamado Suspended. Como es de esperar, si ''Suspended'' es verdadero (''true'') se evitará que el hilo de comience automáticamente tras la creación. Si el valor del parámetro ''Suspended'' es 'false' el hilo se ejecutará inmediatamente después de la creación.  
  
In the constructor, you will prepare the thread to run.
+
   Si el hilo se crea suspendido, entonces sólo se ejecutará después de llamar al método ''Resume''.  
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.
+
   Desde la versión 2.0.1 de PFC '''TThread.Create''' tiene además un parámetro implícito para el tamaño de la pila (''Stack''), con lo que se puede modificar el tamaño de la pila de cada hilo creado si ello es preciso. Un buen ejemplo es la recursión con alto anidamiento de llamadas. Si no se especifica el tamaño de la pila en el parámetro, se utilizará el valor por defecto del SO.
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.
+
   En el método ''Execute'' sobrescrito se pone el código que deseamos que ejecute el hilo.
  
The TThread class has one important property:  
+
&nbsp;&nbsp;&nbsp;La clase TThread tiene una propiedad importante:<syntaxhighlight lang=pascal> Terminated : boolean;</syntaxhighlight>
Terminated : boolean;
 
  
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.
+
&nbsp;&nbsp;&nbsp; Si el hilo tiene un bucle (y esto es lo corriente) este debe terminar cuando ''Terminated'' es cierto (''true''), el estado por defecto es 'false'. Por tanto es preciso comprobar el valor de ''Terminated'', y si es 'true' salir del método ''Execute'' tan pronto y limpiamente como sea posible.
  
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.
+
&nbsp;&nbsp;&nbsp;Hay que tener en cuenta que el método ''Terminate'' no hace nada por defecto: el método ''Execute'' debe realizar explícitamente todas las tareas necesarias para finalizar el trabajo.
  
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.  
+
&nbsp;&nbsp;&nbsp;Como hemos explicado antes, el hilo no debe interactuar con los componentes visibles. Para mostrar algo al usuario hay que hacerlo en el hilo principal. Para hacer esto, TThread tiene un método llamado ''Synchronize'', el cual recibe un parámetro, que consiste en un método de nuestra clase, que a su vez no tiene parámetros. Cuando se llama a este método a través de ''Synchronize(@MyMethod)'', la ejecución del hilo se detendrá, el código de MyMethod se ejecutará en el hilo principal y, a continuación se reanudará la ejecución del hilo.
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.
+
El cómo se ejecuta ''Synchronize'' depende de la plataforma, pero básicamente hace esto: Coloca un mensaje en la cola de mensajes principal y 'se va a dormir'. En su momento, el hilo principal procesa el mensaje y llama a MyMethod. De esta manera se llama MyMethod sin contexto, es decir que no lo hará durante el desplazamiento del ratón o durante el pintado de la ventana, sino después. Después de que el hilo principal ejecuta MyMethod, despierta al hilo del sueño y procesa el siguiente mensaje. En ese momento el hilo continúa su ejecución.
  
Example:
+
&nbsp;&nbsp;&nbsp;Existe otra propiedad importante de ''TThread'': '''FreeOnTerminate'''. Si esta propiedad está a 'true', el hilo se liberará automáticamente al finalizar la ejecución del método 'Execute', en caso contrario hay que liberarlo explícitamente en el código de la aplicación.
  
<delphi>
+
&nbsp;&nbsp;&nbsp;Ejemplo:
  Type
+
 
 +
<syntaxhighlight lang=pascal> Type
 
     TMyThread = class(TThread)
 
     TMyThread = class(TThread)
 
     private
 
     private
 
       fStatusText : string;
 
       fStatusText : string;
       procedure ShowStatus;
+
       procedure ShowStatus;  
 
     protected
 
     protected
 
       procedure Execute; override;
 
       procedure Execute; override;
Line 90: Line 81:
  
 
   procedure TMyThread.ShowStatus;
 
   procedure TMyThread.ShowStatus;
   // this method is executed by the mainthread and can therefore access all GUI elements.
+
   // // este es el método que se llamará desde el hilo principal con Synchronize, para acceder a la interfaz.
 
   begin
 
   begin
 
     Form1.Caption := fStatusText;
 
     Form1.Caption := fStatusText;
Line 99: Line 90:
 
     newStatus : string;
 
     newStatus : string;
 
   begin
 
   begin
     fStatusText := 'TMyThread Starting...';
+
     fStatusText := 'Arrancando TMyThread ...';
 
     Synchronize(@Showstatus);
 
     Synchronize(@Showstatus);
     fStatusText := 'TMyThread Running...';
+
     fStatusText := 'Ejecutando TMyThread...';
     while (not Terminated) and ([any condition required]) do
+
     while (not Terminated) and ([otra_condición_requerida]) do
 
       begin
 
       begin
 
         ...
 
         ...
         [here goes the code of the main thread loop]
+
         //[aquí el código del hilo principal]
 
         ...
 
         ...
 
         if NewStatus <> fStatusText then
 
         if NewStatus <> fStatusText then
Line 113: Line 104:
 
           end;
 
           end;
 
       end;
 
       end;
   end;
+
   end;</syntaxhighlight>
</delphi>
 
  
On the application,
+
&nbsp;&nbsp;&nbsp;En la aplicación,
  
<delphi>
+
<syntaxhighlight lang=pascal> var
  var
 
 
     MyThread : TMyThread;
 
     MyThread : TMyThread;
 
   begin
 
   begin
     MyThread := TMyThread.Create(True); // This way it doesn't start automatically
+
     MyThread := TMyThread.Create(True); // De esta manera no se inicia automáticamente
 
     ...
 
     ...
     [Here the code initialises anything required before the threads starts executing]
+
     //[Aquí el código de inicio necesario antes de que comience la ejecución de los hilos]
 
     ...
 
     ...
 
     MyThread.Resume;
 
     MyThread.Resume;
   end;
+
   end;</syntaxhighlight>
</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:
+
&nbsp;&nbsp;&nbsp;
 +
Si se desea hacer la aplicación más flexible puede crear un evento para el hilo, de esta manera su método sincronizado no estará ligado a un formulario o a una clase específica; y se podrán usar distintos manipuladores para el evento. Aquí hay un ejemplo:
  
<delphi>
+
<syntaxhighlight lang=pascal> Type
  Type
 
 
     TShowStatusEvent = procedure(Status: String) of Object;
 
     TShowStatusEvent = procedure(Status: String) of Object;
  
Line 155: Line 143:
  
 
   procedure TMyThread.ShowStatus;
 
   procedure TMyThread.ShowStatus;
   // this method is executed by the mainthread and can therefore access all GUI elements.
+
   // este es el método que se llamará desde el hilo principal con Synchronize, para acceder a la interfaz
 
   begin
 
   begin
 
     if Assigned(FOnShowStatus) then
 
     if Assigned(FOnShowStatus) then
Line 167: Line 155:
 
     newStatus : string;
 
     newStatus : string;
 
   begin
 
   begin
     fStatusText := 'TMyThread Starting...';
+
     fStatusText := 'Arrancando MyThread...';
 
     Synchronize(@Showstatus);
 
     Synchronize(@Showstatus);
     fStatusText := 'TMyThread Running...';
+
     fStatusText := 'Ejecutando TMyThread...';
     while (not Terminated) and ([any condition required]) do
+
     while (not Terminated) and ([otra_condición_requerida]) do
 
       begin
 
       begin
 
         ...
 
         ...
         [here goes the code of the main thread loop]
+
         //[aquí el código del hilo principal]
 
         ...
 
         ...
 
         if NewStatus <> fStatusText then
 
         if NewStatus <> fStatusText then
Line 181: Line 169:
 
           end;
 
           end;
 
       end;
 
       end;
   end;
+
   end;</syntaxhighlight>
</delphi>
 
  
On the application,
+
&nbsp;&nbsp;&nbsp;En la aplicación,
  
<delphi>
+
<syntaxhighlight lang=pascal> Type
  Type
 
 
     TForm1 = class(TForm)
 
     TForm1 = class(TForm)
 
       Button1: TButton;
 
       Button1: TButton;
Line 223: Line 209:
 
   begin
 
   begin
 
     Label1.Caption := Status;
 
     Label1.Caption := Status;
   end;
+
   end;</syntaxhighlight>
</delphi>
 
 
 
== Special things to take care of ==
 
There is a potential headache in Windows with Threads if you use the -Ct (stack check) switch.
 
For reasons not so clear the stack check will "trigger" on any TThread.Create if you use the default stack size.
 
The only work-around for the moment is to simply not use -Ct switch. Note that it does NOT cause an exception in
 
the main thread, but in the newly created one. This "looks" like if the thread was never started.
 
 
 
A good code to check for this and other exceptions which can occur in thread creation is:
 
  
 +
== Cosas con las que tener especial cuidado ==
 +
&nbsp;&nbsp;&nbsp; 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 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.
  
    MyThread:=TThread.Create(False);
+
&nbsp;&nbsp;&nbsp;Un código adecuado para comprobar si esta u otras excepciones pueden ocurrir en la creación del hilo es:
    if Assigned(MyThread.FatalException) then
 
      raise MyThread.FatalException;
 
  
 +
<syntaxhighlight lang=pascal> MyThread:=TThread.Create(False);
 +
if Assigned(MyThread.FatalException) then
 +
  raise MyThread.FatalException;</syntaxhighlight>
  
This code will asure that any exception which occured during thread creation will be raised in your main thread.
+
&nbsp;&nbsp;&nbsp;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 ==
+
== Unidades necesarias para una aplicación de múltiples hilos==
You don´t need any special unit for this to work with Windows.
+
&nbsp;&nbsp;&nbsp;No es necesaria ninguna unidad específica para trabajar en Windows. Sin embargo, con Linux, MacOSX y FreeBSD, será necesaria la unidad '''cthreads''' que '''debe''' ser la primera en la cláusula '''uses''' del archivo de programa, .lpr del proyecto.
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:
+
&nbsp;&nbsp;&nbsp;Por lo tanto, el código  de la aplicación Lazarus debería ser algo así:
  
  program MyMultiThreadedProgram;
+
<syntaxhighlight lang=pascal> program MiProgramaMultiHilo;
 
   {$mode objfpc}{$H+}
 
   {$mode objfpc}{$H+}
 
   uses
 
   uses
 
   {$ifdef unix}
 
   {$ifdef unix}
 
     cthreads,
 
     cthreads,
 +
    cmem, // el administrador de memoria de c es en algunos sistemas mucho más rápido
 
   {$endif}
 
   {$endif}
 
     Interfaces, // this includes the LCL widgetset
 
     Interfaces, // this includes the LCL widgetset
 
     Forms
 
     Forms
     { add your units here },
+
     { add your units here },</syntaxhighlight>
 +
 
 +
&nbsp;&nbsp;&nbsp;Si no lo haces así, y utilizas TThread, obtendrás este error en el arranque:
 +
  ''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.''
  
If you forget this you will get this error on startup:
+
&nbsp;&nbsp;&nbsp;Nota:
  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:
+
* Si obtienes un error de enlace de <code>"mcount" not found</code> es que se utiliza otra unidad que contiene código de multiproceso y es necesario agregar la unidad cthreads o usar enlace inteligente.
  uses
+
* Si recibes el error: <code>"Project raised exception class 'RunError(232)"</code> en el procedimiento SYSTEM_NOTHREADERROR, es que el código requiere multihilo y hay que añadir la unidad cthreads.
 +
 
 +
===Múltiples hilos en paquetes===
 +
 
 +
&nbsp;&nbsp;&nbsp;Los paquetes que usan múltiples hilos debe añadir el indicador '''-dUseCThreads''' en las opciones del compilador. Abra el paquete, y en el editor, pulsa el botón 'Opciones', en la pestaña 'Uso' y agrega ''-dUseCThreads'' en la opción 'Personalizado' . Con esta operación se definirá  esta opción para todos los proyectos y paquetes que utilicen este paquete, incluyendo el IDE. El IDE y todas las nuevas aplicaciones creadas por el IDE tendrán entonces el siguiente código en su archivo .lpr :
 +
<syntaxhighlight lang=pascal>  uses
 
     {$IFDEF UNIX}{$IFDEF UseCThreads}
 
     {$IFDEF UNIX}{$IFDEF UseCThreads}
 
     cthreads,
 
     cthreads,
     {$ENDIF}{$ENDIF}
+
     {$ENDIF}{$ENDIF}</syntaxhighlight>
  
== SMP Support ==
+
== Soporte para Sistema de Multiproceso Simétrico (''SMP'') ==
The good news is that if your application works properly multithreaded this way, it is already SMP enabled!
 
  
== Debuging Multithreaded Applications with Lazarus ==
+
&nbsp;&nbsp;&nbsp;Las buenas noticias es que si tu aplicación funciona correctamente con múltiples hilos de esta forma, está también preparado para SMP.
The debugging on Lazarus is not fully functional yet.  
 
  
=== Debugging output ===
+
== Depurando aplicaciones de múltiples hilos con Lazarus ==
 +
&nbsp;&nbsp;&nbsp;La depuración en Lazarus necesita GDB y está siendo rápidamente más y más funcional y estable. No obstante, hay distribuciones Linux con algunos problemas.
  
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.
+
=== Salida de la depuración ===
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:
+
&nbsp;&nbsp;&nbsp;En una aplicación de un único hilo simplemente se escribe en la consola o terminal y las líneas son escritas en el orden adecuado. En una aplicación de múltiples hilos las cosas son más complicadas. Si dos hilos están escribiendo, por ejemplo una línea escrita por un hilo A antes que una línea del hilo B, no son necesariamente escritas en ese orden. Puede incluso suceder que un hilo escriba su salida, mientras el otro hilo está escribiendo una línea.
  procedure DbgOutThreadLog(const Msg: string); overload;
+
 
 +
&nbsp;&nbsp;&nbsp;La unidad ''LCLProc'' contiene varias funciones, para que cada hilo escriba su propio archivo de registro:
 +
<syntaxhighlight lang=pascal>  procedure DbgOutThreadLog(const Msg: string); overload;
 
   procedure DebuglnThreadLog(const Msg: string); overload;
 
   procedure DebuglnThreadLog(const Msg: string); overload;
 
   procedure DebuglnThreadLog(Args: array of const); overload;
 
   procedure DebuglnThreadLog(Args: array of const); overload;
   procedure DebuglnThreadLog; overload;
+
   procedure DebuglnThreadLog; overload;</syntaxhighlight>
  
For example:
+
&nbsp;&nbsp;&nbsp;Por ejemplo: En lugar de ''writeln('Algo de texto ',123);'' utiliza  ''DebuglnThreadLog(['Algo de texto ',123])'';, de esta manera se añade la línea 'Algo de texto 123' al archivo '''Log<PID>.txt''', donde PID es el ID del hilo actual.
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.
+
&nbsp;&nbsp;&nbsp;Es una buena idea eliminar los archivos de registro después de cada ejecución:
 
 
It is a good idea to remove the log files before each run:
 
 
   rm -f Log* && ./project1
 
   rm -f Log* && ./project1
  
 
=== Linux ===
 
=== 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:
+
&nbsp;&nbsp;&nbsp;Si intentas depurar una aplicación de múltiples hilos en Linux, tendrás un gran problema: el servidor X se colgará.
 +
&nbsp;&nbsp;&nbsp;Si te ocurre esto, simplemente sal de la sesión y crea una nueva presionando CTRL+ALT+F3. Esto abre una sesión de consola, en ella escribimos '''sudo /etc/init.d/gdm restart'''; con esto rearrancamos el gestor gráfico de escritorio, y regresamos al mismo.
 +
 
 +
&nbsp;&nbsp;&nbsp;Una forma de resolver este problema en ubuntu x64 es configurar las opciones de proyecto en depuración para requerir un fichero de información adicional...
 +
 
 +
  Proyecto -> Opciones del Compilador... -> Enlazando -> Depurando: marcar la opción ''Usar archivo externo de símbolos de depuración para gdb (-Xg)''.
  
Create a new instance of X with:
+
&nbsp;&nbsp;&nbsp;Si lo anterior no funciona prueba este remedio:
 +
 
 +
&nbsp;&nbsp;&nbsp;Crear una nueva instancia del servidor X con:
  
 
   X :1 &
 
   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).
+
&nbsp;&nbsp;&nbsp;Se abrirá un escritorio gráfico, y podrás cambiar al escritorio en que estabas trabajando pulsando CTRL + ALT + F7, y volver al nuevo escritorio con CTRL + ALT + F8 (si esta combinación no funciona, prueba con CTRL + ALT + F2 ... ésto funciona en Slackware)
  
Then you could, if you want, create a desktop session on the X started with:
+
&nbsp;&nbsp;&nbsp;Ahora se puede, si se desea, crear una sesión de escritorio en el servidor X recién arrancado con:
  
 
   gnome-session --display=:1 &
 
   gnome-session --display=:1 &
  
Then, in Lazarus, on the run parameters dialog for the project, check "Use display" and enter :1.
+
&nbsp;&nbsp;&nbsp;Luego, en Lazarus, en los parámetros de ejecución del proyecto en la pestaña Local, marcar "Usar pantalla" y escribimos ':1'.
  
Now the application will run on the seccond X server and you will be able to debug it on the first one.
+
&nbsp;&nbsp;&nbsp;Ahora la aplicación se ejecutará en el segundo servidor X y podremos depurar en el primero.
 
 
This was tested with Free Pascal 2.0 and Lazarus 0.9.10 on Windows and Linux.
 
  
 +
&nbsp;&nbsp;&nbsp;Los procedimientos señalados ha sido probados con  FreePascal 2.0 y Lazarus 0.9.10 en Windows y Linux.
  
 
----
 
----
  
Instead of creating a new X session, one can use [http://en.wikipedia.org/wiki/Xnest 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.
+
&nbsp;&nbsp;&nbsp;En lugar de crear una nueva sesión de X, se puede utilizar [http://en.wikipedia.org/wiki/Xnest Xnest]. Xnest es una sesión X en una ventana. Usándola el servidor X no se bloquea al depurar los hilos, y es mucho más fácil depurar sin tener que cambiar entre terminales.
  
The command line to run Xnest is
+
&nbsp;&nbsp;&nbsp;La segunda línea para ejecutar Xnest es
  
 
   Xnest :1 -ac
 
   Xnest :1 -ac
  
to create a X session on :1, and disabling access control.
+
&nbsp;&nbsp;&nbsp;para crear una sesión X en :1, y deshabilitar el control de acceso.
  
== Widgetsets ==
+
== Artefactos ''Widgetsets'' ==
  
The win32, the gtk and the carbon interfaces fully support multithreading. This means, TThread, critical sections and Synchronize work.
+
&nbsp;&nbsp;&nbsp;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 ==
+
&nbsp;&nbsp;&nbsp;Pero los subprocesos no son seguros. Esto significa que sólo un hilo a la vez puede acceder a la LCL. Y puesto que el hilo principal no debe esperar a otro hilo, sólo al hilo principal se le permite acceder a la LCL, lo que significa que se ocupará de cualquier cosa que tenga que ver con TControl, Application o los componentes de la LCL.
  
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.
+
&nbsp;&nbsp;&nbsp;Hay algunas funciones de hilo seguro en la LCL. Por ejemplo la mayoría de las funciones de la unidad ''FileUtil'' son seguras para subprocesos.
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:
+
== Secciones Críticas==
  
Add the unit SyncObjs.
+
&nbsp;&nbsp;&nbsp;Una ''sección crítica'' es un objeto utilizado para hacer asegurar que cierto código es ejecutado únicamente por un único hilo en un momento dado. La sección círitica debe ser creada e iniciada antes de ser utilizada y debe ser liberada cuándo deje de ser necesaria.
  
Declare the section (globally for all threads which should access the section):
+
&nbsp;&nbsp;&nbsp;Las secciones críticas se utilizan habitualmente de esta forma:
  MyCriticalSection: TRTLCriticalSection;
 
  
Create the section:
+
&nbsp;&nbsp;&nbsp;Añadir la unidad ''SyncObjs''.
  InitializeCriticalSection(MyCriticalSection);
 
  
Run some threads. Doing something exclusively
+
&nbsp;&nbsp;&nbsp;Declarar la sección globalmente para todos los hilos que deban acceder a la sección:
  EnterCriticalSection(MyCriticalSection);
+
 
 +
<syntaxhighlight lang=pascal> MyCriticalSection: TRTLCriticalSection;</syntaxhighlight>
 +
 
 +
&nbsp;&nbsp;&nbsp;Crear e iniciar la sección:
 +
<syntaxhighlight lang=pascal> InitializeCriticalSection(MyCriticalSection);</syntaxhighlight>
 +
 
 +
&nbsp;&nbsp;&nbsp;Ejecutar algunos hilos. Hacer algo que necesite exclusividad
 +
<syntaxhighlight lang=pascal>  EnterCriticalSection(MyCriticalSection);
 
   try
 
   try
     // access some variables, write files, send some network packets, etc
+
     // acceder a algunas variables, escribir archivos, enviar algunos paquetes de red, etc.
 
   finally
 
   finally
 
     LeaveCriticalSection(MyCriticalSection);
 
     LeaveCriticalSection(MyCriticalSection);
   end;
+
   end;</syntaxhighlight>
 +
 
 +
&nbsp;&nbsp;&nbsp;Después de terminar todos los hilos, liberar la sección crítica:
 +
<syntaxhighlight lang=pascal> DeleteCriticalSection(MyCriticalSection);</syntaxhighlight>
 +
 
 +
&nbsp;&nbsp;&nbsp;Como alternativa, puede utilizar un objeto ''TCriticalSection''. La creación del objeto realiza el inicio, el método ''Enter'' realiza la función de ''EnterCriticalSection'', el método ''Leave''  ejecuta ''LeaveCriticalSection'' y la destrucción del objeto lleva a cabo la terminación y liberación de recursos.
 +
 
 +
&nbsp;&nbsp;&nbsp;Por ejemplo: 5 hilos incrementan un contador. Ver ${LazarusDir}/examples/multithreading/criticalsectionexample1.lpi
 +
 
 +
&nbsp;&nbsp;&nbsp;'''Cuidado''': Hay dos conjuntos de las 4 funciones mencionadas. Un conjunto de la RTL y otra de la LCL. Las funciones de la LCL están definidas en las unidades ''LCLIntf'' y ''LCLType''. Ambos  conjuntos trabajan de forma muy parecida. Puede utilizar ambas al mismo tiempo en su aplicación, pero no se debe utilizar una función de RTL con una sección crítica de la LCL, y a la inversa.
 +
 
 +
=== Compartiendo variables ===
 +
 
 +
&nbsp;&nbsp;&nbsp;Si varios hilos comparten una variable, que es de sólo lectura, no hay de que preocuparse, simplemente léela. Pero si uno o varios hilos cambian el valor de la variable, entonces hay que asegurar que únicamente un hilo accede a la variable en un momento dado.
 +
 
 +
&nbsp;&nbsp;&nbsp;Por ejemplo: 5 hilos incrementan un contador. Ver ${LazarusDir}/examples/multithreading/criticalsectionexample1.lpi
 +
 
 +
== Esperando a otro hilo ==
 +
 
 +
&nbsp;&nbsp;&nbsp;En caso de que un hilo A necesite un resultado de otro hilo B, deberá esperar hasta que B termine su ejecución.
 +
 
 +
&nbsp;&nbsp;&nbsp;'''Importante:''' El hilo principal nunca debe esperar a otro hilo. En lugar de eso utilice ''Synchronize'' (ver más arriba)
 +
 
 +
&nbsp;&nbsp;&nbsp;Véase, por ejemplo: ${LazarusDir}/examples/multithreading/waitforexample1.lpi
 +
 
 +
<syntaxhighlight lang=pascal> { TThreadA }
 +
procedure TThreadA.Execute;
 +
  begin
 +
  Form1.ThreadB:=TThreadB.Create(false);
 +
  WaitForB:=RTLEventCreate;        //Crear el evento
 +
  while not Application.Terminated do begin
 +
    RtlEventWaitFor(WaitForB);      // esperar indefinidamente (hasta que A es despertado por B)
 +
    writeln('A: Contador del hilo B='+IntToStr(Form1.ThreadB.Contador));
 +
  end;
 +
end;
 +
 
 +
{ TThreadB }
 +
procedure TThreadB.Execute;
 +
  var
 +
  i: Integer;
 +
  begin
 +
  Contador:=0;
 +
  while not Application.Terminated do begin
 +
    // B: trabajando ...
 +
    Sleep(1500);
 +
    inc(Contador);
 +
    // despertar A
 +
    RtlEventSetEvent(Form1.ThreadA.WaitForB);
 +
  end;
 +
end;</syntaxhighlight>
 +
 
 +
== Réplica (''Fork'') ==
 +
 
 +
&nbsp;&nbsp;&nbsp;Cuándo replicamos una aplicación con múltiples hilos, hay que tener presente que los hilos han de ser creados y estar en funcionamiento '''antes''' de realizar la replica (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;Por lo tanto, hay que tener en cuenta que cualquier hilo iniciado antes de la llamada (incluidos los de la sección de inicio), no va a funcionar.
 +
 
 +
== Computación distribuida ==
  
After all threads terminated, free it:
+
&nbsp;&nbsp;&nbsp;El siguiente gran paso, después de programar múltiples hilos, es hacer que estos se ejecuten en múltiples máquinas.
  DeleteCriticalSection(MyCriticalSection);
+
* Se puede usar algún paquete de TCP como ''synapse'', ''lnet'' o ''Indy'' para las comunicaciones. Esto proporciona la máxima flexibilidad y se usa principalmente para aplicaciones cliente / servidor con conexiones ligeras.
 +
* Se puede usar librerías de envío de mensajes, como [[MPICH]], que se utilizan para Computación de Altas Prestaciones (HPC, ''High Performance Computing'') sobre grupos de ordenadores (''clusters'').
  
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.
+
== Hilos externos ==
  
For example: 5 threads incrementing a counter.
+
&nbsp;&nbsp;&nbsp;Para hacer que el sistema de hilos de Free Pascal funcione correctamente, cada nuevo hilo FPC tiene que ser iniciado (más exactamente, las excepciones, el sistema de E/S y el sistema ''threadvar'' por hilo tiene que ser iniciado ya que ''threadvars'' y la pila ya están trabajando). Esto es totalmente automático si utilizas BeginThread (o indirectamente mediante la clase TThread). Sin embargo, si utilizas los hilos que se crearon sin BeginThread (es decir, hilos externos), trabajo adicional (actualmente) podría ser necesario. En hilos externos se incluyen los que fueron creados en librerías C externas (.DLL/.so). 
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.
+
&nbsp;&nbsp;&nbsp;Cosas ha considerar cuando se utilizan hilos externos (podría no ser necesario en todas o en futuras versiones del compilador):  
  
 +
* No utilices hilos externos en absoluto; usa hilos FPC. Si puedes conseguir el control sobre cómo el hilo se crea, crea el hilo por tí mismo mediante el uso de BeginThread.<br>
 +
Si la convención de llamada no se ajusta (v.g. si tu función original del hilo necesita llamadas con convención cdecl, pero BeginThread necesita convención Pascal, crea un registro, guarda la función requerida original de hilos en ella, y llama a esa función en la función pascal del hilo: 
  
=== Sharing Variables ===
+
<syntaxhighlight lang=pascal> type
 +
  TCdeclThreadFunc = function (user_data:Pointer):Pointer;cdecl;
  
If some threads share a variable, that is read only, then there is nothing to worry about. Just read it.
+
  PCdeclThreadFuncData = ^TCdeclThreadFunc;
But if one or several threads changes the variable, then you must make sure, that only one thread accesses the variables at a time.
+
  TCdeclThreadFuncData = record
 +
    Func: TCdeclThreadFunc;  //cdecl function
 +
    Data: Pointer;          //original data
 +
  end;   
  
For example: 5 threads incrementing a counter.
+
// el hilo pascal llama al función cdecl
See lazarus/examples/multithreading/criticalsectionexample1.lpi
+
function C2P_Translator(FuncData: pointer) : ptrint;
 +
var
 +
  ThreadData: TCdeclThreadFuncData;
 +
begin
 +
  ThreadData := PCdeclThreadFuncData(FuncData)^;
 +
  Result := ptrint(ThreadData.Func(ThreadData.Data));
 +
end;
  
== Waiting for another thread ==
+
procedure CreatePascalThread;
 +
var
 +
  ThreadData: PCdeclThreadFunc;
 +
begin
 +
  New(ThreadData);
 +
  // esta es la función cdecl deseada hilo
 +
  ThreadData^.Func := func;
 +
  ThreadData^.Data := user_data;   
 +
  // this creates the Pascal thread
 +
  BeginThread(@C2P_Translator, ThreadData );
 +
end;</syntaxhighlight>
  
If a thread A needs a result of another thread B, it must wait, till B has finished.  
+
* Inicia el sistema de hilos de FPC mediante la creación de un hilo vacío. Si no creas ningún hilo Pascal en tu aplicación, el sistema de hilos no se puede iniciar (y por tanto threadvars no funcionará y por lo tanto la pila no funcionará correctamente).  
  
'''Important:''' The main thread should never wait for another thread. Instead use Synchronize (see above).
+
<syntaxhighlight lang=pascal> type
 +
  tc = class(tthread)
 +
    procedure execute;override;
 +
  end;
  
See for an example: lazarus/examples/multithreading/waitforexample1.lpi
+
  procedure tc.execute;
 +
    begin
 +
    end;
  
<delphi>
+
{ programa principal }
{ TThreadA }
+
begin
 +
  { Inicia el sistema de hilos }
 +
  with tc.create(false) do
 +
  begin
 +
    waitfor;
 +
    free;
 +
  end;
 +
  { ... aquí sigue tu código }  
 +
end.</syntaxhighlight>
  
procedure TThreadA.Execute;
+
&nbsp;&nbsp;&nbsp;(Después de que el sistema de hilos se inicia, el nucleo de ejecución establece la variable del sistema "IsMultiThread" a verdadero, lo que es utilizado por las rutinas FPC para realizar bloqueos aquí y allá. No se debe establecer esta variable de forma manual)
begin
+
 
  Form1.ThreadB:=TThreadB.Create(false);
+
* Si por alguna razón esto no te funciona, prueba este código en tu función de hilo externo:
  // create event
+
 
  WaitForB:=RTLEventCreate;
+
<syntaxhighlight lang=pascal> function ExternalThread(param: Pointer): LongInt; stdcall;
  while not Application.Terminated do begin
+
var
    // wait infinitely (until B wakes A)
+
  tm: TThreadManager;
    RtlEventWaitFor(WaitForB);
+
begin
    writeln('A: ThreadB.Counter='+IntToStr(Form1.ThreadB.Counter));
+
  GetThreadManager(tm);
  end;
+
  tm.AllocateThreadVars;
end;
+
  InitThread(1000000); // ajustar el tamaño de pila inicial aquí
 +
 
 +
  { hacer algo con hilos aquí ... }
 +
   
 +
  Result:=0;
 +
end;</syntaxhighlight>
  
{ TThreadB }
+
=== Identificando hilo externos ===
 +
&nbsp;&nbsp;&nbsp;A veces incluso no sabes si tienes que tratar con hilos externos (v.g, si alguna librería C hace una retrollamada -callback-). Esto puede ayudar a analizar esto: 
  
procedure TThreadB.Execute;
+
1. Pregunta al S.O. el identificador del subproceso actual al inicio de tu aplicación
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>
 
  
== Fork ==
+
<syntaxhighlight lang=pascal> Win32: GetCurrentThreadID();
 +
Darwin: GetThreadID(); 
 +
Linux: TThreadID(pthread_self);</syntaxhighlight>
  
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.
+
2. Pregunta por el ID del hilo actual, dentro de la función del hilo y compáralo con el resultado del paso 1.
  
So be aware of any threads initializing before the call (including on the initialization section). They will NOT work.
 
  
== Distributed computing ==
+
==See also==
  
The next higher steps after multi threading is running the threads on multiple machines.
+
* [[Streaming components]]
* 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.
+
* [[Manager Worker Threads System]]
* You can use message passing libraries like [[MPICH]], which are used for HPC (High Performance Computing) on clusters.
+
* [[Example of multi-threaded application: array of threads/es|Ejemplo de una aplicación con múltiples hilos: Matriz de hilos]]
 +
* [[Parallel procedures]]
  
[[Category:Castellano]][[Category:Español]]
+
==Contribuciones ==
 +
* Versión en castellano (español) [[User:Iskraelectrica | iskraelectrica (jldc)]] / julio de 2008.

Latest revision as of 00:30, 21 February 2020

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

La clase TThread

   El ejemplo que utilizaremos se puede encontrar en el directorio ${LazarusDir}/examples/multithreading/.

   La manera más fácil de crear una aplicación de múltiples hilos es utilizar la clase TThread. Esta clase permite la creación de un hilo adicional, junto con el hilo principal, de una manera sencilla.

   Por lo general, sólo tienen que sobrescribir dos métodos: el constructor Create y el método Execute.

   En el constructor hay que preparar el hilo para su ejecución. Hay que dar el valor inicial a las variables y propiedades que lo precisen. El constructor original de TThread requiere un parámetro llamado Suspended. Como es de esperar, si Suspended es verdadero (true) se evitará que el hilo de comience automáticamente tras la creación. Si el valor del parámetro Suspended es 'false' el hilo se ejecutará inmediatamente después de la creación.

   Si el hilo se crea suspendido, entonces sólo se ejecutará después de llamar al método Resume.

   Desde la versión 2.0.1 de PFC TThread.Create tiene además un parámetro implícito para el tamaño de la pila (Stack), con lo que se puede modificar el tamaño de la pila de cada hilo creado si ello es preciso. Un buen ejemplo es la recursión con alto anidamiento de llamadas. Si no se especifica el tamaño de la pila en el parámetro, se utilizará el valor por defecto del SO.

   En el método Execute sobrescrito se pone el código que deseamos que ejecute el hilo.

   La clase TThread tiene una propiedad importante:

 Terminated : boolean;

    Si el hilo tiene un bucle (y esto es lo corriente) este debe terminar cuando Terminated es cierto (true), el estado por defecto es 'false'. Por tanto es preciso comprobar el valor de Terminated, y si es 'true' salir del método Execute tan pronto y limpiamente como sea posible.

   Hay que tener en cuenta que el método Terminate no hace nada por defecto: el método Execute debe realizar explícitamente todas las tareas necesarias para finalizar el trabajo.

   Como hemos explicado antes, el hilo no debe interactuar con los componentes visibles. Para mostrar algo al usuario hay que hacerlo en el hilo principal. Para hacer esto, TThread tiene un método llamado Synchronize, el cual recibe un parámetro, que consiste en un método de nuestra clase, que a su vez no tiene parámetros. Cuando se llama a este método a través de Synchronize(@MyMethod), la ejecución del hilo se detendrá, el código de MyMethod se ejecutará en el hilo principal y, a continuación se reanudará la ejecución del hilo.

El cómo se ejecuta Synchronize depende de la plataforma, pero básicamente hace esto: Coloca un mensaje en la cola de mensajes principal y 'se va a dormir'. En su momento, el hilo principal procesa el mensaje y llama a MyMethod. De esta manera se llama MyMethod sin contexto, es decir que no lo hará durante el desplazamiento del ratón o durante el pintado de la ventana, sino después. Después de que el hilo principal ejecuta MyMethod, despierta al hilo del sueño y procesa el siguiente mensaje. En ese momento el hilo continúa su ejecución.

   Existe otra propiedad importante de TThread: FreeOnTerminate. Si esta propiedad está a 'true', el hilo se liberará automáticamente al finalizar la ejecución del método 'Execute', en caso contrario hay que liberarlo explícitamente en el código de la aplicación.

   Ejemplo:

  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;
  // // este es el método que se llamará desde el hilo principal con Synchronize, para acceder a la interfaz.
  begin
    Form1.Caption := fStatusText;
  end;
 
  procedure TMyThread.Execute;
  var
    newStatus : string;
  begin
    fStatusText := 'Arrancando TMyThread ...';
    Synchronize(@Showstatus);
    fStatusText := 'Ejecutando TMyThread...';
    while (not Terminated) and ([otra_condición_requerida]) do
      begin
        ...
        //[aquí el código del hilo principal]
        ...
        if NewStatus <> fStatusText then
          begin
            fStatusText := newStatus;
            Synchronize(@Showstatus);
          end;
      end;
  end;

   En la aplicación,

  var
    MyThread : TMyThread;
  begin
    MyThread := TMyThread.Create(True); // De esta manera no se inicia automáticamente
    ...
    //[Aquí el código de inicio necesario antes de que comience la ejecución de los hilos]
    ...
    MyThread.Resume;
  end;

    Si se desea hacer la aplicación más flexible puede crear un evento para el hilo, de esta manera su método sincronizado no estará ligado a un formulario o a una clase específica; y se podrán usar distintos manipuladores para el evento. Aquí hay un ejemplo:

  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;
  //  este es el método que se llamará desde el hilo principal con Synchronize, para acceder a la interfaz
  begin
    if Assigned(FOnShowStatus) then
    begin
      FOnShowStatus(fStatusText);
    end;
  end;

  procedure TMyThread.Execute;
  var
    newStatus : string;
  begin
    fStatusText := 'Arrancando MyThread...';
    Synchronize(@Showstatus);
    fStatusText := 'Ejecutando TMyThread...';
    while (not Terminated) and ([otra_condición_requerida]) do
      begin
        ...
        //[aquí el código del hilo principal]
        ...
        if NewStatus <> fStatusText then
          begin
            fStatusText := newStatus;
            Synchronize(@Showstatus);
          end;
      end;
  end;

   En la aplicación,

  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;

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

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

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

Unidades necesarias para una aplicación de múltiples hilos

   No es necesaria ninguna unidad específica para trabajar en Windows. Sin embargo, con Linux, MacOSX y FreeBSD, será necesaria la unidad cthreads que debe ser la primera en la cláusula uses del archivo de programa, .lpr del proyecto.

   Por lo tanto, el código de la aplicación Lazarus debería ser algo así:

 program MiProgramaMultiHilo;
  {$mode objfpc}{$H+}
  uses
  {$ifdef unix}
    cthreads,
    cmem, // el administrador de memoria de c es en algunos sistemas mucho más rápido
  {$endif}
    Interfaces, // this includes the LCL widgetset
    Forms
    { add your units here },

   Si no lo haces así, y utilizas TThread, obtendrás este error en el arranque:

 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.

   Nota:

  • Si obtienes un error de enlace de "mcount" not found es que se utiliza otra unidad que contiene código de multiproceso y es necesario agregar la unidad cthreads o usar enlace inteligente.
  • Si recibes el error: "Project raised exception class 'RunError(232)" en el procedimiento SYSTEM_NOTHREADERROR, es que el código requiere multihilo y hay que añadir la unidad cthreads.

Múltiples hilos en paquetes

   Los paquetes que usan múltiples hilos debe añadir el indicador -dUseCThreads en las opciones del compilador. Abra el paquete, y en el editor, pulsa el botón 'Opciones', en la pestaña 'Uso' y agrega -dUseCThreads en la opción 'Personalizado' . Con esta operación se definirá esta opción para todos los proyectos y paquetes que utilicen este paquete, incluyendo el IDE. El IDE y todas las nuevas aplicaciones creadas por el IDE tendrán entonces el siguiente código en su archivo .lpr :

  uses
    {$IFDEF UNIX}{$IFDEF UseCThreads}
    cthreads,
    {$ENDIF}{$ENDIF}

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.

Depurando aplicaciones de múltiples hilos con Lazarus

   La depuración en Lazarus necesita GDB y está siendo rápidamente más y más funcional y estable. No obstante, hay distribuciones Linux con algunos problemas.

Salida de la depuración

   En una aplicación de un único hilo simplemente se escribe en la consola o terminal y las líneas son escritas en el orden adecuado. En una aplicación de múltiples hilos las cosas son más complicadas. Si dos hilos están escribiendo, por ejemplo una línea escrita por un hilo A antes que una línea del hilo B, no son necesariamente escritas en ese orden. Puede incluso suceder que un hilo escriba su salida, mientras el otro hilo está escribiendo una línea.

   La unidad LCLProc contiene varias funciones, para que cada hilo escriba su propio archivo de registro:

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

   Por ejemplo: En lugar de writeln('Algo de texto ',123); utiliza DebuglnThreadLog(['Algo de texto ',123]);, de esta manera se añade la línea 'Algo de texto 123' al archivo Log<PID>.txt, donde PID es el ID del hilo actual.

   Es una buena idea eliminar los archivos de registro después de cada ejecución:

 rm -f Log* && ./project1

Linux

   Si intentas depurar una aplicación de múltiples hilos en Linux, tendrás un gran problema: el servidor X se colgará.    Si te ocurre esto, simplemente sal de la sesión y crea una nueva presionando CTRL+ALT+F3. Esto abre una sesión de consola, en ella escribimos sudo /etc/init.d/gdm restart; con esto rearrancamos el gestor gráfico de escritorio, y regresamos al mismo.

   Una forma de resolver este problema en ubuntu x64 es configurar las opciones de proyecto en depuración para requerir un fichero de información adicional...

 Proyecto -> Opciones del Compilador... -> Enlazando -> Depurando: marcar la opción Usar archivo externo de símbolos de depuración para gdb (-Xg).

   Si lo anterior no funciona prueba este remedio:

   Crear una nueva instancia del servidor X con:

 X :1 &

   Se abrirá un escritorio gráfico, y podrás cambiar al escritorio en que estabas trabajando pulsando CTRL + ALT + F7, y volver al nuevo escritorio con CTRL + ALT + F8 (si esta combinación no funciona, prueba con CTRL + ALT + F2 ... ésto funciona en Slackware)

   Ahora se puede, si se desea, crear una sesión de escritorio en el servidor X recién arrancado con:

 gnome-session --display=:1 &

   Luego, en Lazarus, en los parámetros de ejecución del proyecto en la pestaña Local, marcar "Usar pantalla" y escribimos ':1'.

   Ahora la aplicación se ejecutará en el segundo servidor X y podremos depurar en el primero.

   Los procedimientos señalados ha sido probados con FreePascal 2.0 y Lazarus 0.9.10 en Windows y Linux.


   En lugar de crear una nueva sesión de X, se puede utilizar Xnest. Xnest es una sesión X en una ventana. Usándola el servidor X no se bloquea al depurar los hilos, y es mucho más fácil depurar sin tener que cambiar entre terminales.

   La segunda línea para ejecutar Xnest es

 Xnest :1 -ac

   para crear una sesión X en :1, y deshabilitar el control de acceso.

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.

   Pero los subprocesos no son seguros. Esto significa que sólo un hilo a la vez puede acceder a la LCL. Y puesto que el hilo principal no debe esperar a otro hilo, sólo al hilo principal se le permite acceder a la LCL, lo que significa que se ocupará de cualquier cosa que tenga que ver con TControl, Application o los componentes de la LCL.

   Hay algunas funciones de hilo seguro en la LCL. Por ejemplo la mayoría de las funciones de la unidad FileUtil son seguras para subprocesos.

Secciones Críticas

   Una sección crítica es un objeto utilizado para hacer asegurar que cierto código es ejecutado únicamente por un único hilo en un momento dado. La sección círitica debe ser creada e iniciada antes de ser utilizada y debe ser liberada cuándo deje de ser necesaria.

   Las secciones críticas se utilizan habitualmente de esta forma:

   Añadir la unidad SyncObjs.

   Declarar la sección globalmente para todos los hilos que deban acceder a la sección:

 MyCriticalSection: TRTLCriticalSection;

   Crear e iniciar la sección:

 InitializeCriticalSection(MyCriticalSection);

   Ejecutar algunos hilos. Hacer algo que necesite exclusividad

  EnterCriticalSection(MyCriticalSection);
  try
    // acceder a algunas variables, escribir archivos, enviar algunos paquetes de red, etc.
  finally
    LeaveCriticalSection(MyCriticalSection);
  end;

   Después de terminar todos los hilos, liberar la sección crítica:

 DeleteCriticalSection(MyCriticalSection);

   Como alternativa, puede utilizar un objeto TCriticalSection. La creación del objeto realiza el inicio, el método Enter realiza la función de EnterCriticalSection, el método Leave ejecuta LeaveCriticalSection y la destrucción del objeto lleva a cabo la terminación y liberación de recursos.

   Por ejemplo: 5 hilos incrementan un contador. Ver ${LazarusDir}/examples/multithreading/criticalsectionexample1.lpi

   Cuidado: Hay dos conjuntos de las 4 funciones mencionadas. Un conjunto de la RTL y otra de la LCL. Las funciones de la LCL están definidas en las unidades LCLIntf y LCLType. Ambos conjuntos trabajan de forma muy parecida. Puede utilizar ambas al mismo tiempo en su aplicación, pero no se debe utilizar una función de RTL con una sección crítica de la LCL, y a la inversa.

Compartiendo variables

   Si varios hilos comparten una variable, que es de sólo lectura, no hay de que preocuparse, simplemente léela. Pero si uno o varios hilos cambian el valor de la variable, entonces hay que asegurar que únicamente un hilo accede a la variable en un momento dado.

   Por ejemplo: 5 hilos incrementan un contador. Ver ${LazarusDir}/examples/multithreading/criticalsectionexample1.lpi

Esperando a otro hilo

   En caso de que un hilo A necesite un resultado de otro hilo B, deberá esperar hasta que B termine su ejecución.

   Importante: El hilo principal nunca debe esperar a otro hilo. En lugar de eso utilice Synchronize (ver más arriba)

   Véase, por ejemplo: ${LazarusDir}/examples/multithreading/waitforexample1.lpi

 { TThreadA }
 procedure TThreadA.Execute;
  begin
   Form1.ThreadB:=TThreadB.Create(false);
   WaitForB:=RTLEventCreate;        //Crear el evento
   while not Application.Terminated do begin
     RtlEventWaitFor(WaitForB);       // esperar indefinidamente (hasta que A es despertado por B)
     writeln('A: Contador del hilo B='+IntToStr(Form1.ThreadB.Contador));
   end;
 end;

 { TThreadB }
 procedure TThreadB.Execute;
  var
   i: Integer;
  begin
   Contador:=0;
   while not Application.Terminated do begin
    // B: trabajando ...
    Sleep(1500);
    inc(Contador);
    // despertar A
    RtlEventSetEvent(Form1.ThreadA.WaitForB);
   end;
 end;

Réplica (Fork)

   Cuándo replicamos una aplicación con múltiples hilos, hay que tener presente que los hilos han de ser creados y estar en funcionamiento antes de realizar la replica (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 los de la sección de inicio), no va a funcionar.

Computación distribuida

   El siguiente gran paso, después de programar múltiples hilos, es hacer que estos se ejecuten en múltiples máquinas.

  • Se puede usar algún paquete de TCP como synapse, lnet o Indy para las comunicaciones. Esto proporciona la máxima flexibilidad y se usa principalmente para aplicaciones cliente / servidor con conexiones ligeras.
  • Se puede usar librerías de envío de mensajes, como MPICH, que se utilizan para Computación de Altas Prestaciones (HPC, High Performance Computing) sobre grupos de ordenadores (clusters).

Hilos externos

   Para hacer que el sistema de hilos de Free Pascal funcione correctamente, cada nuevo hilo FPC tiene que ser iniciado (más exactamente, las excepciones, el sistema de E/S y el sistema threadvar por hilo tiene que ser iniciado ya que threadvars y la pila ya están trabajando). Esto es totalmente automático si utilizas BeginThread (o indirectamente mediante la clase TThread). Sin embargo, si utilizas los hilos que se crearon sin BeginThread (es decir, hilos externos), trabajo adicional (actualmente) podría ser necesario. En hilos externos se incluyen los que fueron creados en librerías C externas (.DLL/.so).

   Cosas ha considerar cuando se utilizan hilos externos (podría no ser necesario en todas o en futuras versiones del compilador):

  • No utilices hilos externos en absoluto; usa hilos FPC. Si puedes conseguir el control sobre cómo el hilo se crea, crea el hilo por tí mismo mediante el uso de BeginThread.

Si la convención de llamada no se ajusta (v.g. si tu función original del hilo necesita llamadas con convención cdecl, pero BeginThread necesita convención Pascal, crea un registro, guarda la función requerida original de hilos en ella, y llama a esa función en la función pascal del hilo:

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

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

 // el hilo pascal llama al función cdecl
 function C2P_Translator(FuncData: pointer) : ptrint;
 var
   ThreadData: TCdeclThreadFuncData;
 begin 
   ThreadData := PCdeclThreadFuncData(FuncData)^;
   Result := ptrint(ThreadData.Func(ThreadData.Data));
 end; 

 procedure CreatePascalThread;
 var
   ThreadData: PCdeclThreadFunc; 
 begin 
   New(ThreadData);
   // esta es la función cdecl deseada hilo
   ThreadData^.Func := func; 
   ThreadData^.Data := user_data;    
   // this creates the Pascal thread
   BeginThread(@C2P_Translator, ThreadData );
 end;
  • Inicia el sistema de hilos de FPC mediante la creación de un hilo vacío. Si no creas ningún hilo Pascal en tu aplicación, el sistema de hilos no se puede iniciar (y por tanto threadvars no funcionará y por lo tanto la pila no funcionará correctamente).
 type
   tc = class(tthread)
     procedure execute;override;
   end;

   procedure tc.execute;
    begin
    end;

 { programa principal } 
 begin
  { Inicia el sistema de hilos }
   with tc.create(false) do
   begin
     waitfor;
     free;
   end;
   { ... aquí sigue tu código } 
end.

   (Después de que el sistema de hilos se inicia, el nucleo de ejecución establece la variable del sistema "IsMultiThread" a verdadero, lo que es utilizado por las rutinas FPC para realizar bloqueos aquí y allá. No se debe establecer esta variable de forma manual)

  • Si por alguna razón esto no te funciona, prueba este código en tu función de hilo externo:
 function ExternalThread(param: Pointer): LongInt; stdcall;
 var
   tm: TThreadManager;
 begin
   GetThreadManager(tm);
   tm.AllocateThreadVars;
   InitThread(1000000); // ajustar el tamaño de pila inicial aquí
  
   { hacer algo con hilos aquí ... }
    
   Result:=0;
 end;

Identificando hilo externos

   A veces incluso no sabes si tienes que tratar con hilos externos (v.g, si alguna librería C hace una retrollamada -callback-). Esto puede ayudar a analizar esto:

1. Pregunta al S.O. el identificador del subproceso actual al inicio de tu aplicación

 Win32: GetCurrentThreadID();
 Darwin: GetThreadID();  
 Linux: TThreadID(pthread_self);

2. Pregunta por el ID del hilo actual, dentro de la función del hilo y compáralo con el resultado del paso 1.


See also

Contribuciones