¡Acceso ilimitado 24/7 a todos nuestros libros y vídeos! Descubra la Biblioteca Online ENI. Pulse aquí
¡Acceso ilimitado 24/7 a todos nuestros libros y vídeos! Descubra la Biblioteca Online ENI. Pulse aquí

El multithreading

Introducción

La programación multithread es un dominio apasionante, pero que rápidamente se puede convertir en algo muy complejo de afinar. Varias ejecuciones paralelas en el núcleo de su aplicación deberán compartir información, esperarse e intercambiar datos. El éxito de una arquitectura de este tipo reposa sobre todo en un análisis sólido. Este capítulo no tiene la intención de explicar todas las posibilidades de programación multithread y sus implementaciones en .NET, sino presentar los aspectos fundamentales relacionados con la filosofía POO.

Entender el multithreading

Un programa puede realizar operaciones largas que van a "bloquear" la aplicación durante su ejecución. Para evitar esto, el desarrollador puede crear un tipo de ejecución en paralelo que va a ser responsable de esta operación y, de esta manera, liberar al ejecutor principal. En este caso, el sistema operativo Windows de Microsoft comparte muy rápidamente el tiempo de máquina entre los diferentes flujos de ejecución (típicamente, 20 ms por intervalo de tiempo), dando la impresión de una ejecución simultanea.

Hablamos de sistema operativo en tiempo compartido. El contenido de un hilo de ejecución puede encadenar todas las operaciones que quiera, sin preocuparse por el tiempo que esto llevará a nivel global. El sistema operativo lo interrumpirá periódicamente para dar tiempo al hilo de ejecución siguiente y, de esta manera, continuar hasta volver al primero para que retome su operación donde la dejó.

Tomemos como ejemplo el receptor de entradas/salidas, equipado con una interfaz de programación muy resumida. El constructor nos da un juego de funciones que permite, entre otras cosas, leer el estado binario de las entradas numeradas. Desea desarrollar una aplicación domótica que gestione varias operaciones en paralelo, como la iluminación, la calefacción e incluso la alarma de intrusión....

Multithreading y .NET

La encapsulación de gestión de procesos se proporciona a través del tipo System.Diagnostics.Process.

El siguiente fragmento de código permite ejecutar el programa Windows calc.exe desde un programa .NET utilizando los tipos Process y ProcessStartInfo.


using System;  
using System.Diagnostics;  
  
namespace DemoAppDomain  
{  
  class Program  
  {  
    static void Main(string[] args)  
    {  
      ProcessStartInfo psi = new ProcessStartInfo("calc.exe"); 
      Process.Start(psi);  
  
      Console.ReadKey();  
    }  
  }  
}
 

La clase Process permite realizar otras acciones, como la enumeración de los procesos activos y la lectura de sus características: PID, nombre y número de threads.

Existe una capa intermedia entre el proceso del sistema operativo y su aplicación .NET gestionada. Esta capa se llama dominio de aplicación (AppDomain). Cuando se ejecuta la aplicación, la CLR genera un AppDomain por defecto, que establece la relación con el proceso real del sistema operativo. La mayor parte del tiempo hay tantos procesos como aplicaciones .NET ejecutándose y el desarrollador no tiene que hacer nada particular....

Implementación en C#

Existen tres maneras principales de programar los threads en C#.

1. Uso de un BackgroundWorker

La primera programación de thread consiste en utilizar el editor de recursos de Visual Studio para añadir a un formulario un objeto de tipo System.ComponentModel.BackgroundWorker.

Atención, un thread BackgroundWorker siempre será de tipo background y siempre tendrá una prioridad "normal".

Para crear este tipo de threads de la manera más sencilla posible basta con desplegar el Cuadro de herramientas, seleccionar el BackgroundWorker de la lista de componentes y arrastrarlo al formulario que se está desarrollando.

images/RI08_1.png

Naturalmente, el componente BackgroundWorker no encapsula un control gráfico, aparece debajo de la zona de visualización.

images/RI08_2.png

La codificación de la instanciación del objeto BackgroundWorker se lleva a cabo por el asistente gráfico y el dato miembro toma el nombre por defecto de backgroundWorker1. Ahora, es suficiente con conectar al componente los métodos del formulario que se van a llamar durante la ejecución del thread. Esto se realiza desde la ventana Propiedades de backgroundWorker1...

images/RI08_3.png

Por tanto, la explotación del thread en el formulario pasa por los tres eventos disponibles en el objeto backgroundWorker1:

  • DoWork: apunta al método que se debe ejecutar por el thread.

  • ProcessChanged: apunta a un método que se va a llamar durante la ejecución del trabajo para mostrar la información del avance. Este método puede modificar los componentes del formulario.

  • RunWorkerCompleted: apunta al método que se llama cuando el trabajo termina. También puede modificar la visualización.

Estos tres eventos funcionan con delegate adaptados. El nombre de los delegate corresponde al nombre del evento seguido de EventHandler. Por ejemplo, DoWorkEventHandler es el delegate de DoWork.

Una vez más, el asistente de Visual Studio se ocupa de preparar todo, pero es necesario que conozcamos las posibilidades de comunicación entre la aplicación (el thread principal) y el thread secundario.

a. Comunicación del thread principal con el thread secundario

Durante su ejecución, el thread secundario recibe como argumento un objeto de tipo DoWorkEventArgs, como se puede comprobar en la firma de su delegate DoWork EventHandler.


public delegate void DoWorkEventHandler(object...

Sincronización entre threads

1. Necesidad de la sincronización

La programación de varias rutas de ejecución no plantea ningún problema particular, hasta que comparten la misma información o los mismos recursos. Dando por hecho que el sistema operativo puede interrumpir las operaciones en cualquier momento, se corre el riesgo de tener objetos que se están modificando en un thread prioritario y que se encuentre en algún estado inestable para el thread siguiente. Para prevenir estos funcionamientos incorrectos hay que "sincronizar" los threads, es decir,  proteger las zonas de operación delicadas.

Esto no lo va a realizar el sistema de gestión, que continuará activando los threads unos después de otros; sencillamente, cuando un thread A necesite acceder a una dato común protegido porque un thread B no ha terminado de actualizarlo, el thread A deberá "esperar a la siguiente vuelta". Y si durante la siguiente vuelta el trabajo del thread B no ha terminado todavía, deberá esperar a la siguiente, y así sucesivamente.

El principio es el mismo si se trata de una operación común, de modo que el thread B deberá haber terminado antes de que el thread A lo pueda realizar en su turno. Este es el escenario que muestra el siguiente fragmento de código. De hecho, la operación permite mostrar una cuenta desde cero hasta nueve, realizada por diez threads. El objetivo esperado es la siguiente visualización:


0123456789  
0123456789  
0123456789  
0123456789  
0123456789  
0123456789  
0123456789  
0123456789  
0123456789  
0123456789  
Pulse una tecla para continuar...
 

A continuación se muestra una primera version del código "sin protección":


using System;  
using System.Threading;  
  
namespace PruebaThreadSinSincro  
{  
  class Program  
  {  
    static void Main(string[] args)  
    {  
      Prueba t = new Prueba();  
      t.TratamientoPrincipal();  
    }  
  }  
  
  class Prueba  
  {  
    public...

Comunicación entre threads

En realidad, Lock y Monitor no establecen una comunicación inter-threads. Protegen la aplicación de la ejecución simultánea de secciones críticas y sus secciones críticas se pueden llamar en cualquier momento.

1. Join

El método Join permite a un thread principal "dormirse" mientras espera el final de la ejecución de un thread secundario.

Ejemplo de código:


using System;  
using System.Threading;  
  
namespace SynchroInterThreads  
{  
  class Program  
  {  
    static void Main(string[] args)  
    {  
      Prueba t = new Prueba();  
      t.TratamientoPrincipal();  
    }  
  }  
  
  class Prueba  
  {  
    public void TratamientoPrincipal()  
    {  
      Console.WriteLine("Inicio TratamientoPrincipal");  
      ThreadStart ts  
        = new ThreadStart(TratamientoSecundario);  
      Thread t = new Thread(ts);  
      t.IsBackground = false;  
      t.Priority = ThreadPriority.Highest;  
      t.Name = "Es mi thread :)";  
      t.Start();  
  
      t.Join();  
  
      Console.WriteLine("Fin TratamientoPrincipal");  
    }  
  
    private void TratamientoSecundario()  
    {  
      Console.WriteLine("Inicio TratamientoSecundario");  
      Thread.Sleep(1000 * 10);  
      Console.WriteLine("Fin TratamientoSecundario");  
    }  
  }  
  
}
 

Salida por pantalla asociada:


Inicio TratamientoPrincipal  
Inicio TratamientoSecundario  
Fin TratamientoSecundario  
Fin TratamientoPrincipal  
Pulse una tecla para continuar...
 

Salida pantalla asociada sin la línea t.Join();:


Inicio TratamientoPrincipal  ...

La programación asíncrona

El problema de la notificación mediante función de llamada (callback) es la función de llamada en sí. De hecho, se ejecuta desde un thread diferente y no se puede hacer lo que se quiera.

Desde la versión 4.5 de .NET (Visual Studio 2012) aparecieron las palabras clave async y await, que han simplificado considerablemente la programación asíncrona. De hecho, este dúo permite transformar a bajo coste los métodos bloqueantes y "lentos" y dotarlos de un funcionamiento asíncrono (por tanto no bloqueante) para que la aplicación permanezca activa.

1. La palabra clave async

La palabra clave async informa al compilador de que el método siguiente tendrá un modo de ejecución asíncrono. Por convención no obligatoria, el nombre del método termina con Async para que su lectura recuerde a sus usuarios este modo de funcionamiento particular.

Ejemplo:


private async void TrabajoLargoAsinc(){...
 

2. Contenido de un método async

Un método async tiene un comportamiento "síncrono" hasta que su ejecución encuentra una línea que empieza con la palabra clave await. En este momento, devuelve el control al código que lo invoca sin que haya terminado y la instrucción seguida de la palabra clave await empieza una ejecución asíncrona.

El código final recuerda al de un sencillo método síncrono. Si el método async se llama desde un gestor de eventos gráfico, incluso será posible intervenir sobre los controles...