Share via


Sincronización de subprocesos (C# y Visual Basic)

En las secciones siguientes se describen las características y clases que se pueden utilizar para sincronizar el acceso a recursos en aplicaciones multithreading.

Una de las ventajas de utilizar varios subprocesos en una aplicación es que cada subproceso se ejecuta de forma asincrónica. En las aplicaciones para Windows, esto permite realizar las tareas que exigen mucho tiempo en segundo plano mientras la ventana de la aplicación y los controles siguen respondiendo. En las aplicaciones de servidor, el multithreading proporciona la capacidad de controlar cada solicitud de entrada con un subproceso diferente. De lo contrario, no se atendería cada nueva solicitud hasta que se hubiera satisfecho totalmente la solicitud anterior.

Sin embargo, la naturaleza asincrónica de los subprocesos significa que el acceso a recursos como identificadores de archivos, conexiones de red y memoria se deben coordinar. De lo contrario, dos o más subprocesos podrían tener acceso al mismo tiempo al mismo recurso, cada uno desprevenido de las acciones del otro. El resultado serían daños imprevisibles en los datos.

Para las operaciones simples en tipos de datos numéricos enteros, la sincronización de subprocesos se puede lograr con miembros de la clase Interlocked. Para todos los demás tipos de datos y los recursos no seguros para subprocesos, el multithreading sólo se puede realizar sin ningún riesgo utilizando las estructuras de este tema.

Para obtener información adicional sobre la programación multithreading, vea:

Las palabras clave lock y SyncLock

Las instrucciones lock (C#) y SyncLock (Visual Basic) se puede utilizar para garantizar que un bloque de código se ejecuta hasta el final sin que lo interrumpan otros subprocesos. Esto se logra obteniendo un bloqueo de exclusión mutua para un objeto determinado durante la ejecución de un bloque de código.

Una instrucción lock o SyncLock consiste en dar un objeto como argumento, seguido de un bloque de código que sólo un subproceso puede ejecutar simultáneamente. Por ejemplo:

Public Class TestThreading
    Dim lockThis As New Object

    Public Sub Process()
        SyncLock lockThis
            ' Access thread-sensitive resources.
        End SyncLock
    End Sub
End Class
public class TestThreading
{
    private System.Object lockThis = new System.Object();

    public void Process()
    {

        lock (lockThis)
        {
            // Access thread-sensitive resources.
        }
    }

}

El argumento suministrado a la palabra clave lock tiene que ser un objeto basado en un tipo de referencia y se utiliza para definir el ámbito del bloqueo. En el ejemplo anterior, el ámbito del bloqueo se limita a esta función porque no existe ninguna referencia al objeto lockThis fuera de la función. Si existiese una referencia de ese tipo, el ámbito del bloqueo se extendería a ese objeto. Estrictamente, el objeto suministrado solo se utiliza para identificar únicamente el recurso que varios subprocesos comparten, de modo que puede ser una instancia de clase arbitraria. Sin embargo, en la práctica, este objeto normalmente representa el recurso para el que la sincronización de subprocesos es necesaria. Por ejemplo, si varios subprocesos van a utilizar un objeto contenedor, se puede pasar el contenedor para bloquearlo. Entonces, el bloque de código sincronizado que sigue al bloqueo tendría acceso al contenedor. Con tal de que otros subprocesos bloqueen el mismo contenedor antes de tener acceso a él, el acceso al objeto se sincroniza de forma segura.

Generalmente, es mejor evitar el bloqueo en un tipo public o en instancias de objeto que estén fuera del control de la aplicación. Por ejemplo, lock(this) puede ser problemático si se puede tener acceso a la instancia públicamente, ya que el código que está fuera de su control también puede bloquear el objeto. Esto podría crear situaciones del interbloqueo, en las que dos o más subprocesos esperan a que se libere el mismo objeto. El bloqueo de un tipo de datos público, como opuesto a un objeto, puede producir problemas por la misma razón. El bloqueo de cadenas literales es especialmente arriesgado porque el Common Language Runtime (CLR) interna las cadenas literales. Esto significa que hay una instancia de un literal de cadena determinado para todo el programa, exactamente el mismo objeto representa el literal en todos los dominios de la aplicación en ejecución, en todos los subprocesos. Como resultado, un bloqueo sobre una cadena que tiene el mismo contenido en cualquier parte del proceso de la aplicación bloquea todas las instancias de esa cadena en la aplicación. Por tanto, es mejor bloquear un miembro privado o protegido que no esté internado. Algunas clases proporcionan específicamente los miembros para bloquear. Por ejemplo, el tipo Array proporciona SyncRoot. Muchos tipos de colección también proporcionan un miembro SyncRoot.

Para obtener más información sobre las instrucciones lock y SyncLock, vea los temas:

Monitores

Al igual que las palabras clave lock y SyncLock, los monitores evitan que varios subprocesos ejecuten simultáneamente bloques de código. El método Enter permite que un subproceso, y sólo uno, continué con las instrucciones siguientes; todos los demás subprocesos se bloquean hasta que el subproceso en ejecución llama a Exit. Esto es similar a utilizar la palabra clave lock. Por ejemplo:

SyncLock x
    DoSomething()
End SyncLock
lock (x)
{
    DoSomething();
}

Esto equivale a:

Dim obj As Object = CType(x, Object)
System.Threading.Monitor.Enter(obj)
Try
    DoSomething()
Finally
    System.Threading.Monitor.Exit(obj)
End Try
System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
    DoSomething();
}
finally
{
    System.Threading.Monitor.Exit(obj);
}

Normalmente, es preferible utilizar la palabra clave lock (C#) o SyncLock (Visual Basic) en vez de utilizar directamente la clase Monitor, porque lock o SyncLock es más conciso y porque lock o SyncLock garantiza que se libera el monitor subyacente aunque el código protegido produzca una excepción. Esto se logra con la palabra clave finally, que ejecuta su bloque de código asociado independientemente de que se produzca una excepción.

Eventos de sincronización y controladores de espera

El uso de un bloqueo o un monitor es útil para evitar la ejecución simultánea de bloques de código utilizados por varios subprocesos, pero estas construcciones no permiten que un subproceso comunique un evento a otro. Esto requiere eventos de sincronización, que son objetos que tienen uno de dos estados (señalizado y no señalizado) y se pueden utilizar para activar y suspender subprocesos. Los subprocesos se pueden suspender haciendo que esperen a que se produzca un evento de sincronización que no esté señalizado y se pueden activar cambiando el estado del evento a señalizado. Si un subproceso intenta esperar a que se produzca un evento que ya está señalizado, el subproceso se sigue ejecutando sin retraso.

Hay dos tipos de eventos de sincronización: AutoResetEvent y ManualResetEvent. Sólo difieren en que AutoResetEvent cambia automáticamente de señalizado a no señalizado siempre que activa un subproceso. A la inversa, ManualResetEvent permite que cualquier número de subprocesos esté activado si su estado es señalizado y sólo vuelve al estado no señalizado cuando se llama a su método Reset.

Se puede hacer que los subprocesos esperen en eventos llamando a uno de los métodos de espera como WaitOne, WaitAny o WaitAll. [,Overload:System.Threading.WaitHandle.WaitOne] hace que el subproceso espere hasta que se señalice un único evento, WaitHandle.WaitAny() bloquea un subproceso hasta que se señalicen uno o varios eventos especificados y WaitHandle.WaitAll() bloquea el subproceso hasta que se señalicen todos los eventos indicados. Un evento se señaliza cuando se llama a su método Set.

En el ejemplo siguiente, la función Main crea e inicia un subproceso. El nuevo subproceso espera a que se produzca un evento mediante el método WaitOne. Se suspende el subproceso hasta que el evento sea señalizado por el subproceso primario que está ejecutando la función Main. Cuando el evento se señaliza, vuelve a ejecutarse el subproceso auxiliar. En este caso, como el evento sólo se utiliza para una activación del subproceso, se podrían utilizar las clases AutoResetEvent o ManualResetEvent.

Imports System.Threading

Module Module1
    Dim autoEvent As AutoResetEvent

    Sub DoWork()
        Console.WriteLine("   worker thread started, now waiting on event...")
        autoEvent.WaitOne()
        Console.WriteLine("   worker thread reactivated, now exiting...")
    End Sub

    Sub Main()
        autoEvent = New AutoResetEvent(False)

        Console.WriteLine("main thread starting worker thread...")
        Dim t As New Thread(AddressOf DoWork)
        t.Start()

        Console.WriteLine("main thread sleeping for 1 second...")
        Thread.Sleep(1000)

        Console.WriteLine("main thread signaling worker thread...")
        autoEvent.Set()
    End Sub
End Module
using System;
using System.Threading;

class ThreadingExample
{
    static AutoResetEvent autoEvent;

    static void DoWork()
    {
        Console.WriteLine("   worker thread started, now waiting on event...");
        autoEvent.WaitOne();
        Console.WriteLine("   worker thread reactivated, now exiting...");
    }

    static void Main()
    {
        autoEvent = new AutoResetEvent(false);

        Console.WriteLine("main thread starting worker thread...");
        Thread t = new Thread(DoWork);
        t.Start();

        Console.WriteLine("main thread sleeping for 1 second...");
        Thread.Sleep(1000);

        Console.WriteLine("main thread signaling worker thread...");
        autoEvent.Set();
    }
}

Objeto Mutex

Una exclusión mutua es similar a un monitor; impide la ejecución simultánea de un bloque de código por más de un subproceso a la vez. De hecho, el nombre "mutex" es una forma abreviada del término "mutuamente exclusivo". Sin embargo, a diferencia de los monitores, una exclusión mutua se puede utilizar para sincronizar los subprocesos entre varios procesos. Una exclusión mutua se representa mediante la clase Mutex.

Cuando se utiliza para la sincronización entre procesos, una exclusión mutua se denomina una exclusión mutua con nombre porque va a utilizarla otra aplicación y, por tanto, no se puede compartir por medio de una variable global o estática. Se debe asignar un nombre para que ambas aplicaciones puedan tener acceso al mismo objeto de exclusión mutua.

Aunque se puede utilizar una exclusión mutua para la sincronización de subprocesos dentro de un proceso, normalmente es preferible utilizar Monitor porque los monitores se diseñaron específicamente para .NET Framework y, por tanto, hacen un mejor uso de los recursos. Por el contrario, la clase Mutex es un contenedor para una construcción de Win32. Aunque es más eficaz que un monitor, la exclusión mutua requiere transiciones de interoperabilidad, que utilizan más recursos del sistema que la clase Monitor. Para obtener un ejemplo de uso de la exclusión mutua, vea Exclusiones mutuas (mutex).

Clase Interlocked

Puede utilizar los métodos de la clase Interlocked para evitar problemas que pueden producirse cuando varios subprocesos intentan actualizar o comparar simultáneamente el mismo valor. Los métodos de esta clase permiten incrementar, reducir, intercambiar y comparar valores, de forma segura, desde cualquier subproceso

Bloqueos ReaderWriterLock

En algunos casos, quizá desee bloquear un recurso sólo mientras se están escribiendo los datos y permitir que múltiples clientes puedan leer datos simultáneamente cuando no se estén actualizando los datos. La clase ReaderWriterLock fuerza el acceso exclusivo a un recurso mientras hay un subproceso modificando el recurso, pero permite el acceso no exclusivo al leer el recurso. Los bloqueos de ReaderWriter son una alternativa útil a los bloqueos exclusivos que hacen esperar a otros subprocesos, incluso cuando esos subprocesos no necesitan actualizar datos.

Interbloqueos

La sincronización de subprocesos resulta de un valor incalculable en aplicaciones multiproceso, pero siempre existe el peligro de crear un deadlock, en el que varios subprocesos están esperando unos a otros y la aplicación se bloquea. Un interbloqueo es una situación análoga a otra en la que hay automóviles parados en un cruce con cuatro señales de stop y cada uno de los conductores está esperando a que los otros se pongan en marcha. Evitar los interbloqueos es importante; la clave está en una cuidadosa planificación. A menudo es posible prevenir situaciones de interbloqueo mediante la creación de diagramas de las aplicaciones multiproceso, antes de empezar a escribir código.

Secciones relacionadas

Cómo: Usar un grupo de subprocesos (C# y Visual Basic)

Cómo sincronizar el acceso a un recurso compartido en un entorno de multithreading con C#

Cómo crear un subproceso mediante C#

Cómo enviar un elemento de trabajo al grupo de subprocesos mediante C#

Cómo sincronizar el acceso a un recurso compartido en un entorno de multithreading con C#

Vea también

Referencia

SyncLock (Instrucción)

lock (Instrucción, Referencia de C#)

Thread

WaitOne

WaitAny

WaitAll

Join

Start

Sleep

Monitor

Mutex

AutoResetEvent

ManualResetEvent

Interlocked

WaitHandle

EventWaitHandle

System.Threading

Set

Conceptos

Aplicaciones multiproceso (C# y Visual Basic)

Exclusiones mutuas (mutex)

Monitores

Operaciones de bloqueo

AutoResetEvent

Sincronizar datos para subprocesamiento múltiple

Otros recursos

Implementing the CLR Asynchronous Programming Model

Simplified APM with C#

Deadlock monitor

Subprocesamiento múltiple en componentes

HOW TO: Synchronize Access to a Shared Resource in a Multithreading Environment by Using Visual C# .NET