8. Los hilos de ejecución
8.1. Introducción
Cuando se inicia una aplicación, esta se ejecuta en un flujo de ejecución llamado hilo. La clase .NET que modela un thread es la clase System.Threading.Thread y tiene la siguiente definición:

Solo utilizaremos algunas de las propiedades y métodos de esta clase:
devuelve el hilo que se está ejecutando actualmente | |
nombre del hilo | |
indica si el hilo está activo (true) o no (false) | |
inicia la ejecución de un hilo | |
detiene definitivamente la ejecución de un hilo | |
detiene la ejecución de un hilo durante n milisegundos | |
suspende temporalmente la ejecución de un hilo | |
reanuda la ejecución de un hilo suspendido | |
Operación bloqueante: espera a que termine el hilo para pasar a la siguiente instrucción |
Veamos una primera aplicación que destaca 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 tiene acceso a las características de este hilo mediante Thread.CurrentThread
- el papel 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 hilos de ejecución
Es posible tener aplicaciones en las que fragmentos de código se ejecutan de manera «simultánea» en diferentes hilos de ejecución. Cuando se dice que los thread se ejecutan de manera simultánea, a menudo se comete un error de lenguaje. Si la máquina solo tiene un procesador, como suele ser el caso, los thread comparten ese procesador: cada uno dispone de él, por turnos, durante un breve instante (unos milisegundos). Esto es lo que da la ilusión de paralelismo en la ejecución. El tiempo asignado a un thread depende de diversos factores, entre ellos su prioridad, que tiene un valor predeterminado pero que también puede fijarse mediante programación. Cuando un thread tiene acceso al 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 estado de suspensión durante un tiempo determinado (sleep)
- Un hilo T se crea primero mediante su constructor
ThreadStart es de tipo delegado y define el prototipo de una función sin parámetros:
Una construcción clásica es la siguiente:
La función run, pasada como parámetro, se ejecutará al iniciar el hilo.
- La ejecución del hilo T se inicia mediante T.Start(): la función [run], pasada al constructor de T, será ejecutada por el hilo T. El programa que ejecuta la instrucción T.start() no espera a que termine la tarea T: pasa inmediatamente a la siguiente instrucción. 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 conjunto que deben realizar. Este es el problema de la sincronización de los hilos.
- Una vez iniciado, el hilo thread se ejecuta de manera autónoma. Se detendrá cuando la función start que está ejecutando haya terminado su trabajo.
- Se pueden enviar ciertas señales a la tarea T:
- T.Suspend() le indica que se detenga momentáneamente
- T.Resume() le indica que reanude su trabajo
- T.Abort() le indica que se detenga definitivamente
- 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 una forma de sincronización.
Analicemos el siguiente programa:
' opciones
Option Strict On
Option Explicit On
' espacios de nombres
Imports System
Imports System.Threading
Module thread2
Public Sub Main()
' inicializar el hilo actual
Dim main As Thread = Thread.CurrentThread
' asignación de un nombre al hilo
main.Name = "main"
' creación de hilos 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 función
Console.Out.WriteLine(("fin du thread " + main.Name))
End Sub
Public Sub affiche()
' se muestra el 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")))
' se pone en espera durante 1 s
Thread.Sleep(1000)
' se muestra el 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 hilo no es bloqueante. El método Main inició la ejecución de 5 hilos en paralelo y terminó su ejecución antes que ellos. La operación
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 hilos creados deben ejecutar el método affiche. El orden de ejecución es impredecible. Aunque en el ejemplo el orden de ejecución parezca seguir el orden de las solicitudes de ejecución, no se pueden extraer conclusiones generales. El sistema operativo cuenta aquí con 6 hilos y un procesador. Distribuirá el procesador entre estos 6 hilos según sus propias reglas.
- En los resultados se observa una consecuencia del método Sleep. En el ejemplo, es el hilo 0 el que ejecuta primero el método affiche. Se muestra el mensaje de inicio de ejecución y luego ejecuta el método Sleep, que lo suspende durante 1 segundo. Entonces pierde el procesador, que queda así disponible para otro hilo. El ejemplo muestra que es el hilo 1 el que lo obtendrá. El hilo 1 seguirá el mismo recorrido, al igual que los demás hilos. Cuando termine el segundo de espera del hilo 0, su ejecución podrá reanudarse. El sistema le asigna el procesador y puede terminar la ejecución del método affiche.
Modifiquemos nuestro programa para finalizar el método Main con las instrucciones:
' fin de la rutina principal
Console.Out.WriteLine(("fin du thread " + main.Name))
Environment.Exit(0)
Al ejecutar el nuevo programa, se obtiene:
Los subprocesos creados por la función Main no se ejecutan. Es la instrucción
la que provoca esto: elimina todos los hilos de la aplicación y no solo el hilo Main. La solución a este problema es que el método Main espere a que terminen de ejecutarse los hilos que ha creado antes de finalizar él mismo. Esto se puede lograr con el método Join de la clase Thread:
' se espera a que terminen de ejecutarse todos los hilos
For i = 0 To tâches.Length - 1
' esperando el fin de la ejecución del hilo i
tâches(i).Join()
Next i 'for
' fin de la rutina principal
Console.Out.WriteLine(("fin du thread " + main.Name))
Environment.Exit(0)
De este modo, se obtienen 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. Importancia de los hilos
Ahora que hemos destacado la existencia de un hilo por defecto —el que ejecuta el método Main— y que sabemos cómo crear otros, detengámonos en la utilidad que tienen los hilos para nosotros y en la razón por la que los presentamos aquí. Hay un tipo de aplicaciones que se prestan muy 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 siguen 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 manera 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 tardar 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 específico. 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 de cliente-servidor mencionado anteriormente, cada hilo atiende a un cliente de manera prácticamente independiente. Sin embargo, los hilos pueden verse obligados a cooperar para prestar el servicio solicitado a su cliente, especialmente para acceder 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 agente atiende a un cliente. Supongamos que, de vez en cuando, estos agentes deben hacer fotocopias de documentos que traen sus clientes y que solo hay una fotocopiadora. Dos empleados no pueden usar la fotocopiadora al mismo tiempo. Si el empleado i encuentra que la fotocopiadora está siendo utilizada por el empleado j, tendrá que esperar. A esta situación se le llama «acceso a un recurso compartido» y, en informática, es bastante difícil de manejar. Veamos el siguiente ejemplo:
- una aplicación generará n subprocesos, donde n se pasa como parámetro
- el recurso compartido es un contador que deberá ser incrementado por cada hilo generado
- Al finalizar la aplicación, se muestra el valor del contador. Por lo tanto, deberíamos encontrar 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)
' detener
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))
' nombramiento
threads(i).Name = "tache_" & i
' inicio
threads(i).Start()
Next i
' espera a que terminen los hilos
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, que utiliza cada subproceso para incrementar el contador estático cptrThreads.
- Se lee el contador
- el hilo se detiene durante 1 s. Por lo tanto, pierde el procesador
- se incrementa el contador
El paso 2 solo sirve para obligar al hilo a perder el control del procesador. Este se le asignará a otro hilo. En la práctica, no hay garantía de que un hilo no sea 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, por lo tanto, solo sirve 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 analizar estos resultados, queda claro lo que está sucediendo:
- un primer hilo lee el contador. Encuentra 0.
- Se detiene durante 1 s, por lo que cede el procesador
- entonces, un segundo hilo toma el control del procesador y también lee el valor del contador. Sigue en 0, ya que el hilo anterior aún no lo ha incrementado. Este también se detiene durante 1 s.
- En 1 s, los 5 hilos tienen tiempo de ejecutarse todos y leer el valor 0.
- Cuando se activen uno tras otro, incrementarán el valor 0 que leyeron y escribirán el valor 1 en el contador, lo cual confirma el programa principal (Main).
¿De dónde surge el problema? El segundo hilo leyó un valor incorrecto debido a que el primero había sido interrumpido antes de terminar su tarea, que consistía en actualizar el contador en 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 retenido 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 que se encuentra entre la lectura del contador y la escritura de su nuevo valor:
' lectura del contador
Dim valeur As Integer = cptrThreads
' en espera
Thread.Sleep(1000)
' incremento del contador
cptrThreads = valeur + 1
Para ejecutar este código, se debe garantizar que un hilo esté solo. Puede ser interrumpido, pero durante esa interrupción, ningún otro hilo 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:

Aquí solo utilizaremos los siguientes constructores y métodos:
crea un objeto de sincronización M | |
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 (lo cual ocurre al inicio), se le “asigna” al hilo T1 que lo solicitó. Si un poco más tarde, 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 tiene en su poder. De esta manera, varios hilos pueden quedar bloqueados a la espera del mutex M. | |
El hilo T1, que realiza la operación M.ReleaseMutex(), deja de tener la propiedad del mutex M.Lorsque; el hilo T1 perderá el procesador, el sistema podrá asignárselo a uno de los hilos que esperan el mutex M. Solo uno lo obtendrá a su vez; los demás que esperan el mutex M permanecerán bloqueados |
Un mutex M administra el acceso a un recurso compartido R. Un hilo solicita el recurso R mediante M.WaitOne() y lo libera 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 de la siguiente manera:
donde M es un objeto Mutex. Por supuesto, nunca se debe olvidar liberar un Mutex que ya no sea necesario para que otro hilo pueda ingresar a la sección crítica; de lo contrario, los hilos que esperan un mutex que nunca se libera nunca tendrán acceso al procesador. Además, hay que evitar la situación de interbloqueo (deadlock) en la que dos hilos se esperan mutuamente. Consideremos las siguientes acciones que se suceden en el tiempo:
- un hilo T1 obtiene la propiedad de un mutex M1 para tener acceso a un recurso compartido R1
- un hilo T2 obtiene la propiedad de un mutex M2 para acceder a un recurso compartido R2
- el hilo T1 solicita el mutex M2. Queda bloqueado.
- El hilo T2 solicita el mutex M1. Está bloqueado.
En este caso, los hilos T1 y T2 se esperan mutuamente. Este caso se presenta cuando los hilos 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 ocupación prolongada 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 aplicamos lo que acabamos de ver al ejemplo anterior, nuestra aplicación queda así:
' opciones
Option Explicit On
Option Strict On
' uso de hilos
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)
' detener
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))
' asignación de nombre
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 ingresar a 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 la autorización de acceso
autorisation.ReleaseMutex()
End Sub
End Class
Los resultados obtenidos coinciden con 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.
- Tenemos una tabla en la que unos procesos almacenan datos (los productores) y otros los leen (los consumidores).
- Los productores son iguales entre sí, pero exclusivos: solo un productor a la vez puede depositar sus datos en la tabla.
- Los consumidores son iguales entre sí, pero exclusivos: solo un lector a la vez puede leer los datos almacenados en la tabla.
- 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 ya están allí han sido consumidos.
En esta exposición se pueden distinguir dos recursos compartidos:
- la tabla en modo de escritura
- la tabla de lectura
El acceso a estos dos recursos compartidos puede controlarse mediante mutex, como se vio anteriormente, uno para cada recurso. Una vez que un consumidor ha obtenido la tabla en lectura, debe verificar que efectivamente haya datos en ella. Se utilizará un evento para notificárselo. 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:

Este tipo de evento es similar a un valor booleano, pero evita las esperas activas o semiactivas. Así, si el derecho de escritura se controla mediante un valor booleano peutEcrire, un productor, antes de escribir, ejecutará un código del tipo:
o
En el primer método, el hilo ocupa innecesariamente el procesador. En el segundo, verifica el estado de la variable booleana 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
inicializa el valor booleano peutEcrire a false. La operación
ejecutada por un hilo hace que este continúe si la variable booleana peutEcrire es verdadera; de lo contrario, queda bloqueado hasta que se vuelva verdadera. Otro hilo 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 generarán
Const nbThreads As Integer = 3
' inicialización de los 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 los hilos 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 los hilos 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 la tabla
Dim i As Integer
For i = 0 To data.Length - 1
'espera de 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 la sección crítica
lecteur.ReleaseMutex()
End Sub
' escribir en la tabla
Public Shared Sub écrire()
' sección crítica
' solo un escritor puede pasar
é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 de 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 la sección crítica
écrivain.ReleaseMutex()
End Sub
End Class
La ejecución arroja 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, solo hay un lector a la vez, aunque este pierde el procesador en la sección crítica lire
- efectivamente, 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

