Programación multihilo en Delphi. TThread y sincronización básica

Introducción

Si no lo has leido ya, y eres relativamente nuevo al mundo de la programación multihilo es recomendable empezar leyendo la Introducción a la programación multihilo para poder decidir correctamente si realmente es necesario implementar un sistema multihilo o no.

Definiendo nuestro hilo

Delphi facilita mucho la creación de hilos de ejecución proporcionando una clase base que podemos heredar para definir nuestras tareas de ejecución. Esta clase es la clase TThread.

Un ejemplo de una aplicación que usa una tarea para comprimir un archivo.

{ Heredamos la clase TThread y definimos lo que queremos que
   el hilo haga haciendo un override del metodo Execute }

type TMiThread = class(TThread)
  private
    FFileName : String;
  public
    { El constructor de la clase
      CreateSuspended : Si la tarea se crea suspendida
      FileName : El fichero sobre el que se aplica la tarea
    }

    constructor Create(CreateSuspended : boolean;
                       FileName : string); override;
    procedure Execute; override;
end;

TForm1 = class(TForm)
    BtnComprimir: TButton;
    procedure BtnComprimirClick(Sender: TObject);
  private
    { Private declarations }
    thr1, thr2 : TMyThread;
  public
    { Public declarations }
end;

constructor TMiThread.Create(CreateSuspended : boolean);
begin
  // Inicializar los que queramos
  FFileName = FileName;
  inherited Create(CreateSuspended);
end;

procedure Execute;
begin
  // El código que ejecutará la tarea
  // Por ejemplo comprimir el fichero pasado
end;

procedure TForm1.OnBtnComprimirClick(Sender : TComponent);
begin
  // Creamos la tarea suspendida
  thr1 = TMyThread.Create(true,"MiFichero.txt");
  thr2 = TMyThread.Create(true,"Otro.txt");
  // Iniciar las tareas
  thr1.Resume;
  thr2.Resume;
  // Esperar a que acaben
  if not thr1.Terminated then
    thr1.WaitFor;
  if not thr2.Terminated then
    thr2.WaitFor;
end;

En el código anterior podemos observar con que basta con crear una clase que herede de la clase TThread y realizar un override del metodo Execute poniendo en él el código que queramos que realice la tarea. Este código suele seguir generalmente este patron:

procedure Execute;
begin
  { Ejecutar algún proceso repetitivo hasta que la tarea termine }
  while not Self.Terminated do
  begin
     RealizarTrabajo;
  end;
  FinalizarRecursos; // Esto puede ir aqui (si los recursos son privados)
                               // o en el destructor (si otro hilo quiere acceder al resultado)
end;

Cuando se llama al metodo Terminate de la clase TThread, el valor de Terminated se pone a true y la función Execute se sale. Aunque existen formas de "matar" hilos mediante el API de windows la forma más común (y más recomendable) de actuación es "solicitar" al hilo su terminación (generalmente desde el hilo principal) llamanda al metodo Terminate y después quedarse a la espera de que el hilo finalice (llamando al metodo WaitFor)

De esta forma podemos ver que la creación de hilos en Delphi es una cosa muy sencilla y que, de forma rápida, podemos distrubir la carga de trabajo entre varias tareas.

Sincronización básica

Uno de las complicaciones de la programación multihilo es la problemática asociada al acceso simultaneo a las variables compartidas y la necesidad de sincronizar dichos accesos de forma que no haya conflictos.

La problemática de los acceso a variables compartidas es un problema común proveniente de la necesidad de atomicidad en algunas operaciones. Por ejemplo, analizando la operación

  i := i + 1;

que incrementa una variable global i en uno, (que por otro lado es uno de los ejemplos más típicos que se pueden poner) observamos que, si dos hilos de ejecucíón intentan ejecutar el código simultaneamente podría pasar que:

  • Entra el hilo 1 y lee el valor de i, por ejemplo 7, (y lo almacena en un registro de máquina)
  • Justo en ese momento se produce un cambio de contexto y entra a ejecutar el hilo 2
  • El hilo 2 lee el valor de i (7), le suma 1 y lo almacena
  • Vuelve a entrar el hilo 1, recupera el valor del registro de la máquina (7), le suma 1 y lo almacena

El resultado final obtenido será 8 cuando lo que esperariamos obtener si entrarán dos hilos cuando el valor es 7 sería 9.

Una forma básica de solucionar este problema es utilizar el metodo Synchronize de la clase TThread. Dicho metodo produce la ejecución del metodo especificado desde el hilo de ejecución principal de forma que no haya conflictos. De esta forma la llamada anterior quedaría como:

procedure IncrementaI;
begin
  i := i + 1;
end;

procedure PulsaBoton;
begin
  Button1.Click();
end;

procedure TMyThread.Execute;
begin
  // ........... Código de ejecución
  // ..............................
  // Aqui incremento i
  Self.Synchronize(IncrementaI);
  // Y pulso el boton del formulario
  Self.Synchronize(PulsaBoton);
end;

De esta forma evitamos cualquier tipo de problema en la sincronización de acceso a los recursos.

Nota:
Casi todos los componentes VCL de delphi (botones, formularios, treeviews, etc) no son ThreadSafe, es decir, no soportan ser invocados desde distintos hilos, por ello, toda llamada a algún metodo de un componente debe ser protegída mediante el metodo Synchronize.
8.75
Average: 8.8 (4 votes)
Your rating: None