Multithreaded Application Tutorial/es

From Lazarus wiki
Jump to navigationJump to search

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

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

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:<delphi> Terminated : boolean;</delphi>

    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:

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

   En la aplicación,

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

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

   En la aplicación,

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

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

<delphi> program MyMultiThreadedProgram;

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

   Si no se hace así, se obtendrá 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.

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

Depurando aplicaciones de múltiples hilos con Lazarus

   La depuración con Lazarus no es todavía completamente funcional

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: <delphi> procedure DbgOutThreadLog(const Msg: string); overload;

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

   Por 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á.

   Se desconoce la forma de resolver este problema correctamente, pero hay un 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.

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:

<delphi> MyCriticalSection: TRTLCriticalSection;</delphi>

   Crear e iniciar la sección: <delphi> InitializeCriticalSection(MyCriticalSection);</delphi>

   Ejecutar algunos hilos. Hacer algo que necesite exclusividad <delphi> EnterCriticalSection(MyCriticalSection);

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

   Después de terminar todos los hilos, liberar la sección crítica: <delphi> DeleteCriticalSection(MyCriticalSection);</delphi>

   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

<delphi> { 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;</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 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 en la sección de inicio). No van 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).

Contribuciones