Skip to content

8. Los subprocesos de ejecución

8.1. Introducción

Cuando se inicia una aplicación, esta se ejecuta en un flujo de ejecución denominado subproceso. La clase .NET que modela un thread es la clase System.Threading.Thread y tiene la siguiente definición:

Image

Solo utilizaremos algunas de las propiedades y métodos de esta clase:

CurrentThread - propriété statique
devuelve el hilo que se está ejecutando actualmente
Name - propriété d'objet
nombre del hilo
isAlive - propriété d'objet
indica si el hilo está activo (true) o no (false)
Start - méthode d'objet
inicia la ejecución de un hilo
Abort - méthode d'objet
detiene definitivamente la ejecución de un hilo
Sleep(n) - méthode statique
detiene la ejecución de un hilo durante n milisegundos
Suspend() - méthode d'objet
suspende temporalmente la ejecución de un hilo
Resume() - méthode d'objet
reanuda la ejecución de un hilo suspendido
Join() - méthode d'objet
Operación bloqueante: espera a que finalice el hilo para pasar a la siguiente instrucción

Veamos una primera aplicación que pone de manifiesto la existencia de un hilo principal de ejecución, aquel en el que se ejecuta la función Main de una clase:


' uso de subprocesos
Imports System
Imports System.Threading

Public Module thread1
    Public Sub Main()
        ' inicialización del hilo actual
        Dim main As Thread = Thread.CurrentThread
        ' visualización
        Console.Out.WriteLine(("Thread courant : " + main.Name))
        ' se cambia el nombre
        main.Name = "main"
        ' verificación
        Console.Out.WriteLine(("Thread courant : " + main.Name))
        ' bucle infinito
        While True
            ' visualización
            Console.Out.WriteLine((main.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
            ' parada temporal
            Thread.Sleep(1000)
        End While
    End Sub
End Module

Resultados en pantalla:

dos>thread1
Thread courant :
Thread courant : main
main : 06:13:55
main : 06:13:56
main : 06:13:57
main : 06:13:58
main : 06:13:59

El ejemplo anterior ilustra los siguientes puntos:

  • la función Main se ejecuta correctamente en un hilo
  • se puede acceder a las características de este hilo mediante Thread.CurrentThread
  • la función del método Sleep. Aquí, el hilo que ejecuta Main entra en suspensión regularmente durante 1 segundo entre dos visualizaciones.

8.2. Creación de subprocesos de ejecución

Es posible tener aplicaciones en las que fragmentos de código se ejecutan de forma «simultánea» en diferentes subprocesos de ejecución. Cuando se dice que los thread se ejecutan de forma simultánea, a menudo se comete un abuso de lenguaje. Si la máquina solo tiene un procesador, como suele ser el caso, los thread comparten ese procesador: disponen de él, cada uno por turnos, durante un breve instante (unos milisegundos). Esto es lo que da la ilusión de paralelismo de ejecución. El tiempo asignado a un thread depende de diversos factores, entre ellos su prioridad, que tiene un valor por defecto pero que también puede fijarse mediante programación. Cuando un thread dispone del procesador, normalmente lo utiliza durante todo el tiempo que se le ha asignado. Sin embargo, puede liberarlo antes de tiempo:

  • poniéndose en espera de un evento (wait, join, suspend)
  • entrando en suspensión durante un tiempo determinado (sleep)
  1. Un hilo T se crea primero mediante su constructor
Public Sub New(ByVal start As ThreadStart)

ThreadStart es de tipo delegado y define el prototipo de una función sin parámetros:

Public Delegate Sub ThreadStart()

Una construcción clásica es la siguiente:

dim T as Thread=new Thread(new ThreadStart(run));

La función run pasada como parámetro se ejecutará al iniciar el hilo.

  1. La ejecución del subproceso T se inicia mediante T.Start(): la función [run] pasada al constructor de T será entonces ejecutada por el hilo T. El programa que ejecuta la instrucción T.start() no espera a que finalice la tarea T: pasa inmediatamente a la instrucción siguiente. Así, tenemos dos tareas que se ejecutan en paralelo. A menudo deben poder comunicarse entre sí para saber en qué punto se encuentra el trabajo común que deben realizar. Este es el problema de la sincronización de subprocesos.
  1. Una vez iniciado, el hilo se ejecuta de forma autónoma. Se detendrá cuando la función start que está ejecutando haya terminado su trabajo.
  1. Se pueden enviar ciertas señales a la tarea T:
    1. T.Suspend() le indica que se detenga momentáneamente
    2. T.Resume() le indica que reanude su trabajo
    3. T.Abort() le indica que se detenga definitivamente
  1. También se puede esperar a que termine su ejecución mediante T.join(). Se trata de una instrucción bloqueante: el programa que la ejecuta queda bloqueado hasta que la tarea T haya terminado su trabajo. Es un medio de sincronización.

Examinemos el siguiente programa:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System
Imports System.Threading

Module thread2
    Public Sub Main()
        ' inicialización del hilo actual
        Dim main As Thread = Thread.CurrentThread
        ' asignación de nombre al hilo
        main.Name = "main"

        ' creación de subprocesos de ejecución
        Dim tâches(4) As Thread
        Dim i As Integer
        For i = 0 To tâches.Length - 1
            ' se crea el hilo i
            tâches(i) = New Thread(New ThreadStart(AddressOf affiche))
            ' se establece el nombre del hilo
            tâches(i).Name = "tache_" & i
            ' se inicia la ejecución del hilo i
            tâches(i).Start()
        Next i
        ' fin de la rutina
        Console.Out.WriteLine(("fin du thread " + main.Name))
    End Sub

    Public Sub affiche()
        ' visualización del inicio de la ejecución
        Console.Out.WriteLine(("Début d'exécution de la méthode affiche dans le Thread " + Thread.CurrentThread.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
        ' suspensión durante 1 s
        Thread.Sleep(1000)
        ' visualización del final de la ejecución
        Console.Out.WriteLine(("Fin d'exécution de la méthode affiche dans le Thread " + Thread.CurrentThread.Name + " : " + DateTime.Now.ToString("hh:mm:ss")))
    End Sub
End Module

El hilo principal, el que ejecuta la función Main, crea otros 5 hilos encargados de ejecutar el método estático affiche. Los resultados son los siguientes:

dos>thread2
fin du thread main
Début d'exécution de la méthode affiche dans le Thread tache_0 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_1 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_2 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_3 : 05:27:53
Début d'exécution de la méthode affiche dans le Thread tache_4 : 05:27:53
Fin d'exécution de la méthode affiche dans le Thread tache_0 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_1 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_2 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_3 : 05:27:54
Fin d'exécution de la méthode affiche dans le Thread tache_4 : 05:27:54

Estos resultados son muy reveladores:

  • en primer lugar, se observa que el inicio de la ejecución de un subproceso no es bloqueante. El método Main inició la ejecución de 5 subprocesos en paralelo y terminó su ejecución antes que ellos. La operación
            ' se inicia la ejecución del hilo i
            tâches(i).Start()

inicia la ejecución del hilo tareas[i], pero una vez hecho esto, la ejecución continúa inmediatamente con la instrucción siguiente sin esperar a que finalice la ejecución del hilo.

  • Todos los subprocesos creados deben ejecutar el método affiche. El orden de ejecución es impredecible. Aunque en el ejemplo el orden de ejecución parece seguir el orden de las solicitudes de ejecución, no se pueden extraer conclusiones generales. El sistema operativo tiene aquí 6 subprocesos y un procesador. Distribuirá el procesador entre estos 6 subprocesos según sus propias reglas.
  • En los resultados se observa una consecuencia del método Sleep. En el ejemplo, es el subproceso 0 el que ejecuta primero el método affiche. Se muestra el mensaje de inicio de ejecución y, a continuación, ejecuta el método Sleep, que lo suspende durante 1 segundo. Entonces pierde el procesador, que queda así disponible para otro subproceso. El ejemplo muestra que es el subproceso 1 el que lo obtendrá. El subproceso 1 seguirá el mismo recorrido, al igual que los demás subprocesos. Cuando finalice el segundo de espera del subproceso 0, su ejecución podrá reanudarse. El sistema le cede el procesador y puede terminar la ejecución del método affiche.

Modifiquemos nuestro programa para terminar el método Main con las instrucciones:

        ' fin de la mano
        Console.Out.WriteLine(("fin du thread " + main.Name))
        Environment.Exit(0)

La ejecución del nuevo programa da como resultado:

fin du thread main

Los subprocesos creados por la función Main no se ejecutan. Es la instrucción

        Environment.Exit(0)

la que provoca esto: elimina todos los subprocesos de la aplicación y no solo el subproceso Main. La solución a este problema es que el método Main espere a que finalice la ejecución de los subprocesos que ha creado antes de terminarse él mismo. Esto se puede hacer con el método Join de la clase Thread:


        ' se espera a que finalice la ejecución de todos los subprocesos
        For i = 0 To tâches.Length - 1
            ' esperando el final de la ejecución del hilo i
            tâches(i).Join()
        Next i        'for
        ' fin de la rutina
        Console.Out.WriteLine(("fin du thread " + main.Name))
        Environment.Exit(0)

Se obtienen entonces los siguientes resultados:

Début d'exécution de la méthode affiche dans le Thread tache_1 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_2 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_3 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_4 : 05:34:48
Début d'exécution de la méthode affiche dans le Thread tache_0 : 05:34:48
Fin d'exécution de la méthode affiche dans le Thread tache_2 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_1 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_3 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_0 : 05:34:50
Fin d'exécution de la méthode affiche dans le Thread tache_4 : 05:34:50
fin du thread main

8.3. Interés de los subprocesos

Ahora que hemos puesto de manifiesto la existencia de un hilo por defecto, el que ejecuta el método Main, y que sabemos cómo crear otros, detengámonos en el interés que tienen para nosotros los hilos y en la razón por la que los presentamos aquí. Hay un tipo de aplicaciones que se prestan bien al uso de subprocesos: las aplicaciones cliente-servidor de Internet. En una aplicación de este tipo, un servidor ubicado en una máquina S1 responde a las solicitudes de clientes ubicados en máquinas remotas C1, C2, ..., Cn.

Todos los días utilizamos aplicaciones de Internet que se ajustan a este esquema: servicios web, correo electrónico, consulta de foros, transferencia de archivos... En el esquema anterior, el servidor S1 debe atender a los clientes Ci de forma simultánea. Si tomamos el ejemplo de un servidor FTP (Protocolo de Transferencia de Archivos) que entrega archivos a sus clientes, sabemos que una transferencia de archivos puede llevar a veces varias horas. Por supuesto, es impensable que un solo cliente monopolice el servidor durante tanto tiempo. Lo que se suele hacer es que el servidor cree tantos subprocesos de ejecución como clientes haya. Cada subproceso se encarga entonces de atender a un cliente concreto. Dado que el procesador se comparte cíclicamente entre todos los subprocesos activos de la máquina, el servidor dedica un poco de tiempo a cada cliente, garantizando así la simultaneidad del servicio.

8.4. Acceso a recursos compartidos

En el ejemplo cliente-servidor mencionado anteriormente, cada subproceso atiende a un cliente de forma prácticamente independiente. No obstante, los subprocesos pueden verse obligados a cooperar para prestar el servicio solicitado a su cliente, especialmente para el acceso a recursos compartidos. El esquema anterior recuerda a las ventanillas de una gran administración, como una oficina de correos, por ejemplo, donde en cada ventanilla un empleado atiende a un cliente. Supongamos que, de vez en cuando, estos empleados tienen que hacer fotocopias de documentos que traen sus clientes y que solo hay una fotocopiadora. Dos empleados no pueden utilizar la fotocopiadora al mismo tiempo. Si el empleado i encuentra la fotocopiadora ocupada por el empleado j, tendrá que esperar. A esta situación se le denomina «acceso a un recurso compartido» y, en informática, resulta bastante delicada de gestionar. Tomemos el siguiente ejemplo:

  • una aplicación va a generar n subprocesos, siendo n un parámetro
  • el recurso compartido es un contador que deberá ser incrementado por cada subproceso generado
  • al final de la aplicación, se muestra el valor del contador. Por lo tanto, deberíamos obtener n.

El programa es el siguiente:


' opciones
Option Explicit On 
Option Strict On

' uso de subprocesos
Imports System
Imports System.Threading

Public Class thread3
    ' variables de clase
    Private Shared cptrThreads As Integer = 0

    Public Overloads Shared Sub Main(ByVal args() As [String])
        ' manual de instrucciones
        Const syntaxe As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100

        ' verificación del número de argumentos
        If args.Length <> 1 Then
            ' error
            Console.Error.WriteLine(syntaxe)
            ' parada
            Environment.Exit(1)
        End If
        ' verificación de la calidad del argumento
        Dim nbThreads As Integer = 0
        Try
            nbThreads = Integer.Parse(args(0))
            If nbThreads < 1 Or nbThreads > nbMaxThreads Then
                Throw New Exception
            End If
        Catch
            ' error
            Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et " & nbMaxThreads & ")")
            ' fin
            Environment.Exit(2)
        End Try
        ' creación y generación de subprocesos
        Dim threads(nbThreads - 1) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' creación
            threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
            ' denominación
            threads(i).Name = "tache_" & i
            ' inicio
            threads(i).Start()
        Next i
        ' espera a que finalicen los subprocesos
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i        ' affichage compteur
        Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
    End Sub

    Public Shared Sub incrémente()
        ' incrementa el contador de subprocesos
        ' lectura del contador
        Dim valeur As Integer = cptrThreads
        ' seguimiento
        Console.Out.WriteLine(("A " + DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
        ' espera
        Thread.Sleep(1000)
        ' incremento del contador
        cptrThreads = valeur + 1
        ' seguimiento
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
    End Sub
End Class

No nos detendremos en la parte de generación de subprocesos, que ya hemos estudiado. Centrémonos más bien en el método incrémente, utilizado por cada subproceso para incrementar el contador estático cptrThreads.

  1. Se lee el contador
  2. el subproceso se detiene durante 1 s. Por lo tanto, pierde el procesador
  3. se incrementa el contador

El paso 2 solo sirve para obligar al hilo a perder el procesador. Este se asignará a otro hilo. En la práctica, nada garantiza que un hilo no vaya a ser interrumpido entre el momento en que lee el contador y el momento en que lo incrementa. Existe el riesgo de perder el procesador entre el momento en que se lee el valor del contador y el momento en que se escribe su valor incrementado en 1. De hecho, la operación de incremento será objeto de varias instrucciones elementales a nivel del procesador que pueden ser interrumpidas. El paso 2 de espera de un segundo solo sirve, por tanto, para sistematizar este riesgo. Los resultados obtenidos son los siguientes:

dos>thread3 5
A 05:44:34, le thread tache_0 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_1 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_2 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_3 a lu la valeur du compteur : 0
A 05:44:34, le thread tache_4 a lu la valeur du compteur : 0
A 05:44:35, le thread tache_0 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_1 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_2 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_3 a écrit la valeur du compteur : 1
A 05:44:35, le thread tache_4 a écrit la valeur du compteur : 1
Nombre de threads générés : 1

Al leer estos resultados, se ve claramente lo que ocurre:

  • un primer hilo lee el contador. Encuentra 0.
  • Se detiene durante 1 s, por lo que pierde el procesador
  • A continuación, un segundo subproceso toma el control del procesador y también lee el valor del contador. Este sigue estando a 0, ya que el subproceso anterior aún no lo ha incrementado. Este también se detiene durante 1 s.
  • En 1 s, los 5 subprocesos tienen tiempo de pasar todos y leer el valor 0.
  • Cuando se reanuden uno tras otro, incrementarán el valor 0 que han leído y escribirán el valor 1 en el contador, lo que confirma el programa principal (Main).

¿De dónde viene el problema? El segundo hilo ha leído un valor erróneo debido a que el primero se interrumpió antes de haber terminado su trabajo, que consistía en actualizar el contador de la ventana. Esto nos lleva al concepto de recurso crítico y sección crítica de un programa:

  • un recurso crítico es un recurso que solo puede ser poseído por un hilo a la vez. En este caso, el recurso crítico es el contador.
  • Una sección crítica de un programa es una secuencia de instrucciones en el flujo de ejecución de un hilo durante la cual este accede a un recurso crítico. Debemos asegurarnos de que, durante esta sección crítica, sea el único en tener acceso al recurso.

8.5. Acceso exclusivo a un recurso compartido

En nuestro ejemplo, la sección crítica es el código situado entre la lectura del contador y la escritura de su nuevo valor:


        ' lectura del contador
        Dim valeur As Integer = cptrThreads
        ' espera
        Thread.Sleep(1000)
        ' incremento del contador
        cptrThreads = valeur + 1

Para ejecutar este código, es necesario garantizar que un subproceso esté solo. Puede ser interrumpido, pero durante esa interrupción, ningún otro subproceso debe poder ejecutar ese mismo código. La plataforma .NET ofrece varias herramientas para garantizar el acceso único a las secciones críticas del código. Utilizaremos la clase Mutex:

Image

Aquí solo utilizaremos los siguientes constructores y métodos:

public Mutex()
crea un objeto de sincronización M
public bool WaitOne()
El hilo T1, que ejecuta la operación M.WaitOne(), solicita la propiedad del objeto de sincronización M. Si ningún hilo tiene el control del mutex M (como ocurre inicialmente), se «cede» al hilo T1 que lo ha solicitado. Si, poco después, un hilo T2 realiza la misma operación, quedará bloqueado. De hecho, un mutex solo puede pertenecer a un hilo. Se desbloqueará cuando el hilo T1 libere el mutex M que mantiene. De este modo, varios hilos pueden quedar bloqueados a la espera del mutex M.
public void
ReleaseMutex()
El hilo T1 que realiza la operación M.ReleaseMutex() abandona la propiedad del mutex M.Lorsque; el hilo T1 perderá el procesador, el sistema podrá asignárselo a uno de los subprocesos en espera del mutex M. Solo uno lo obtendrá a su vez, mientras que los demás que esperan el mutex M permanecerán bloqueados

Un mutex M gestiona el acceso a un recurso compartido R. Un hilo solicita el recurso R mediante M.WaitOne() y lo devuelve mediante M.ReleaseMutex(). Una sección crítica de código que solo debe ser ejecutada por un único hilo a la vez es un recurso compartido. La sincronización de la ejecución de la sección crítica puede realizarse así:

M.WaitOne()
' el hilo es el único que entra aquí
' sección crítica
....
M.ReleaseMutex()

donde M es un objeto Mutex. Por supuesto, nunca hay que olvidar liberar un Mutex que ya no sea necesario para que otro subproceso pueda entrar en la sección crítica; de lo contrario, los subprocesos que esperan un mutex que nunca se libera nunca tendrán acceso al procesador. Por otra parte, hay que evitar la situación de interbloqueo (deadlock) en la que dos subprocesos se esperan mutuamente. Consideremos las siguientes acciones que se suceden en el tiempo:

  • un hilo T1 obtiene la propiedad de un mutex M1 para acceder a un recurso compartido R1
  • un subproceso T2 obtiene la propiedad de un mutex M2 para acceder a un recurso compartido R2
  • el subproceso T1 solicita el mutex M2. Queda bloqueado.
  • El subproceso T2 solicita el mutex M1. Queda bloqueado.

Aquí, los subprocesos T1 y T2 se esperan mutuamente. Este caso se da cuando los subprocesos necesitan dos recursos compartidos: el recurso R1 controlado por el mutex M1 y el recurso R2 controlado por el mutex M2. Una posible solución es solicitar ambos recursos al mismo tiempo mediante un único mutex M. Sin embargo, esto no siempre es posible si, por ejemplo, implica una larga ocupación de un recurso costoso. Otra solución es que un hilo que tenga M1 y no pueda obtener M2, libere entonces M1 para evitar el interbloqueo. Si ponemos en práctica lo que acabamos de ver en el ejemplo anterior, nuestra aplicación queda así:


' opciones
Option Explicit On 
Option Strict On

' uso de subprocesos
Imports System
Imports System.Threading

Public Class thread4
    ' variables de clase
    Private Shared cptrThreads As Integer = 0    ' compteur de threads
    Private Shared autorisation As Mutex

    Public Overloads Shared Sub Main(ByVal args() As [String])
        ' manual de instrucciones
        Const syntaxe As String = "pg nbThreads"
        Const nbMaxThreads As Integer = 100

        ' verificación del número de argumentos
        If args.Length <> 1 Then
            ' error
            Console.Error.WriteLine(syntaxe)
            ' parada
            Environment.Exit(1)
        End If
        ' verificación de la calidad del argumento
        Dim nbThreads As Integer = 0
        Try
            nbThreads = Integer.Parse(args(0))
            If nbThreads < 1 Or nbThreads > nbMaxThreads Then
                Throw New Exception
            End If
        Catch
        End Try

        ' inicialización de la autorización de acceso a una sección crítica
        autorisation = New Mutex

        ' creación y generación de subprocesos
        Dim threads(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' creación
            threads(i) = New Thread(New ThreadStart(AddressOf incrémente))
            ' denominación
            threads(i).Name = "tache_" & i
            ' inicio
            threads(i).Start()
        Next i
        ' espera a que finalicen los subprocesos
        For i = 0 To nbThreads - 1
            threads(i).Join()
        Next i
        ' visualización del contador
        Console.Out.WriteLine(("Nombre de threads générés : " & cptrThreads))
    End Sub

    Public Shared Sub incrémente()
        ' incrementa el contador de subprocesos
        ' se solicita permiso para entrar en la sección crítica
        autorisation.WaitOne()
        ' lectura del contador
        Dim valeur As Integer = cptrThreads
        ' seguimiento
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a lu la valeur du compteur : " & cptrThreads))
        ' espera
        Thread.Sleep(1000)
        ' incremento del contador
        cptrThreads = valeur + 1
        ' seguimiento
        Console.Out.WriteLine(("A " & DateTime.Now.ToString("hh:mm:ss") & ", le thread " & Thread.CurrentThread.Name & " a écrit la valeur du compteur : " & cptrThreads))
        ' se concede el permiso de acceso
        autorisation.ReleaseMutex()
    End Sub
End Class

Los resultados obtenidos se ajustan a lo esperado:

dos>thread4 5
A 05:51:10, le thread tache_0 a lu la valeur du compteur : 0
A 05:51:11, le thread tache_0 a écrit la valeur du compteur : 1
A 05:51:11, le thread tache_1 a lu la valeur du compteur : 1
A 05:51:12, le thread tache_1 a écrit la valeur du compteur : 2
A 05:51:12, le thread tache_2 a lu la valeur du compteur : 2
A 05:51:13, le thread tache_2 a écrit la valeur du compteur : 3
A 05:51:13, le thread tache_3 a lu la valeur du compteur : 3
A 05:51:14, le thread tache_3 a écrit la valeur du compteur : 4
A 05:51:14, le thread tache_4 a lu la valeur du compteur : 4
A 05:51:15, le thread tache_4 a écrit la valeur du compteur : 5
Nombre de threads générés : 5

8.6. Sincronización por eventos

Consideremos la siguiente situación, a veces denominada situación de productores-consumidores.

  1. Tenemos una tabla en la que unos procesos depositan datos (los productores) y otros los leen (los consumidores).
  2. Los productores son iguales entre sí pero exclusivos: solo un productor a la vez puede depositar sus datos en la tabla.
  3. Los consumidores son iguales entre sí pero exclusivos: solo un lector a la vez puede leer los datos depositados en la tabla.
  4. Un consumidor solo puede leer los datos de la tabla cuando un productor los ha depositado en ella, y un productor solo puede depositar nuevos datos en la tabla cuando los que hay en ella han sido consumidos.

En esta exposición se pueden distinguir dos recursos compartidos:

    1. la tabla en escritura
    2. la tabla de lectura

El acceso a estos dos recursos compartidos puede controlarse mediante mutex, como se ha visto anteriormente, uno para cada recurso. Una vez que un consumidor ha obtenido la tabla en lectura, debe comprobar que efectivamente hay datos en ella. Se utilizará un evento para avisarle. Del mismo modo, un productor que haya obtenido la tabla en escritura deberá esperar a que un consumidor la haya vaciado. Aquí también se utilizará un evento.

Los eventos utilizados formarán parte de la clase AutoResetEvent:

Image

Este tipo de evento es análogo a un booleano, pero evita esperas activas o semiactivas. Así, si el derecho de escritura está controlado por un booleano peutEcrire, un productor, antes de escribir, ejecutará un código del tipo:

while(peutEcrire==false)        ' attente active

o

while(peutEcrire==false) ' attente semi-active
    Thread.Sleep(100)                ' attente de 100ms
end while

En el primer método, el hilo ocupa innecesariamente el procesador. En el segundo, comprueba el estado del booleano peutEcrire cada 100 ms. La clase AutoResetEvent permite mejorar aún más las cosas: el hilo solicitará que se le despierte cuando se produzca el evento que está esperando:

AutoEvent peutEcrire=new AutoResetEvent(false)        ' peutEcrire=false;
....
peutEcrire.WaitOne() ' le thread attend que l'évt peutEcrire passe à vrai

La operación

AutoEvent peutEcrire=new AutoResetEvent(false)        ' peutEcrire=false;

inicializa el valor booleano peutEcrire a false. La operación

peutEcrire.WaitOne() ' le thread attend que l'évt peutEcrire passe à vrai

ejecutada por un subproceso hace que este pase si el valor booleano peutEcrire es verdadero; de lo contrario, se bloquea hasta que se vuelva verdadero. Otro subproceso lo establecerá en verdadero mediante la operación peutEcrire.Set() o en falso mediante la operación peutEcrire.Reset().

El programa de productores-consumidores es el siguiente:


' uso de subprocesos de lectura y escritura
' ilustra el uso simultáneo de recursos compartidos y sincronización

' opciones
Option Explicit On 
Option Strict On

' uso de subprocesos
Imports System
Imports System.Threading

Public Class lececr

    ' variables de clase
    Private Shared data(5) As Integer    ' ressource partagée entre threads lecteur et threads écrivain
    Private Shared lecteur As Mutex    ' variable de synchronisation pour lire le tableau
    Private Shared écrivain As Mutex    ' variable de synchronisation pour écrire dans le tableau
    Private Shared objRandom As New Random(DateTime.Now.Second)    ' un générateur de nombres aléatoires
    Private Shared peutLire As AutoResetEvent    ' signale qu'on peut lire le contenu de data
    Private Shared peutEcrire As AutoResetEvent

    Public Shared Sub Main(ByVal args() As [String])

        ' el número de subprocesos que se van a generar
        Const nbThreads As Integer = 3

        ' inicialización de indicadores
        peutLire = New AutoResetEvent(False)        ' on ne peut pas encore lire
        peutEcrire = New AutoResetEvent(True)        ' on peut déjà écrire

        ' inicialización de las variables de sincronización
        lecteur = New Mutex         ' synchronise les lecteurs
        écrivain = New Mutex         ' synchronise les écrivains

        ' creación de subprocesos de lectura
        Dim lecteurs(nbThreads) As Thread
        Dim i As Integer
        For i = 0 To nbThreads - 1
            ' creación
            lecteurs(i) = New Thread(New ThreadStart(AddressOf lire))
            lecteurs(i).Name = "lecteur_" & i
            ' inicio
            lecteurs(i).Start()
        Next i

        ' creación de subprocesos de escritura
        Dim écrivains(nbThreads) As Thread
        For i = 0 To nbThreads - 1
            ' creación
            écrivains(i) = New Thread(New ThreadStart(AddressOf écrire))
            écrivains(i).Name = "écrivain_" & i
            ' inicio
            écrivains(i).Start()
        Next i

        'fin de la mano
        Console.Out.WriteLine("fin de Main...")
    End Sub

    ' leer el contenido de la tabla
    Public Shared Sub lire()
        ' sección crítica
        lecteur.WaitOne()        ' un seul lecteur peut passer
        peutLire.WaitOne()        ' on doit pouvoir lire

        ' lectura de tabla
        Dim i As Integer
        For i = 0 To data.Length - 1
            'espera 1 s
            Thread.Sleep(1000)
            ' visualización
            Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : Le lecteur " & Thread.CurrentThread.Name & " a lu le nombre " & data(i)))
        Next i

        ' ya no se puede leer
        peutLire.Reset()
        ' se puede escribir
        peutEcrire.Set()
        ' fin de sección crítica
        lecteur.ReleaseMutex()
    End Sub

    ' escribir en la tabla
    Public Shared Sub écrire()
        ' sección crítica
        ' solo puede pasar un escritor
        écrivain.WaitOne()
        ' hay que esperar la autorización para escribir
        peutEcrire.WaitOne()

        ' escritura en la tabla
        Dim i As Integer
        For i = 0 To data.Length - 1
            'espera 1 s
            Thread.Sleep(1000)
            ' visualización
            data(i) = objRandom.Next(0, 1000)
            Console.Out.WriteLine((DateTime.Now.ToString("hh:mm:ss") & " : L'écrivain " & Thread.CurrentThread.Name & " a écrit le nombre " & data(i)))
        Next i

        ' ya no se puede escribir
        peutEcrire.Reset()
        ' se puede leer
        peutLire.Set()
        'fin de sección crítica
        écrivain.ReleaseMutex()
    End Sub
End Class

La ejecución da los siguientes resultados:

dos>lececr
fin de Main...
05:56:56 : L'écrivain écrivain_0 a écrit le nombre 459
05:56:57 : L'écrivain écrivain_0 a écrit le nombre 955
05:56:58 : L'écrivain écrivain_0 a écrit le nombre 212
05:56:59 : L'écrivain écrivain_0 a écrit le nombre 297
05:57:00 : L'écrivain écrivain_0 a écrit le nombre 37
05:57:01 : L'écrivain écrivain_0 a écrit le nombre 623
05:57:02 : Le lecteur lecteur_0 a lu le nombre 459
05:57:03 : Le lecteur lecteur_0 a lu le nombre 955
05:57:04 : Le lecteur lecteur_0 a lu le nombre 212
05:57:05 : Le lecteur lecteur_0 a lu le nombre 297
05:57:06 : Le lecteur lecteur_0 a lu le nombre 37
05:57:07 : Le lecteur lecteur_0 a lu le nombre 623
05:57:08 : L'écrivain écrivain_1 a écrit le nombre 549
05:57:09 : L'écrivain écrivain_1 a écrit le nombre 34
05:57:10 : L'écrivain écrivain_1 a écrit le nombre 781
05:57:11 : L'écrivain écrivain_1 a écrit le nombre 555
05:57:12 : L'écrivain écrivain_1 a écrit le nombre 812
05:57:13 : L'écrivain écrivain_1 a écrit le nombre 406
05:57:14 : Le lecteur lecteur_1 a lu le nombre 549
05:57:15 : Le lecteur lecteur_1 a lu le nombre 34
05:57:16 : Le lecteur lecteur_1 a lu le nombre 781
05:57:17 : Le lecteur lecteur_1 a lu le nombre 555
05:57:18 : Le lecteur lecteur_1 a lu le nombre 812
05:57:19 : Le lecteur lecteur_1 a lu le nombre 406
05:57:20 : L'écrivain écrivain_2 a écrit le nombre 442
05:57:21 : L'écrivain écrivain_2 a écrit le nombre 83
^C

Se pueden observar los siguientes puntos:

  • efectivamente hay un solo lector a la vez, aunque este pierde el procesador en la sección crítica lire
  • solo hay un escritor a la vez, aunque este pierde el procesador en la sección crítica écrire
  • un lector solo lee cuando hay algo que leer en la tabla
  • un escritor solo escribe cuando la tabla se ha leído por completo