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:

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 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)
- 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 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.
- 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.
- 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 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
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:
La ejecución del nuevo programa da como resultado:
Los subprocesos creados por la función Main no se ejecutan. Es la instrucción
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.
- Se lee el contador
- el subproceso 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 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:

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 (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. | |
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í:
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.
- Tenemos una tabla en la que unos procesos depositan 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 depositados 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 hay en ella han sido consumidos.
En esta exposición se pueden distinguir dos recursos compartidos:
- la tabla en escritura
- 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:

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:
o
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
inicializa el valor booleano peutEcrire a false. La operación
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

