Sincronización de subprocesos (Guía de programación de C#)

Actualización: noviembre 2007

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

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 subprocesamiento múltiple 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 subprocesamiento múltiple sólo se puede realizar sin ningún riesgo utilizando las estructuras de este tema.

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

La palabra clave de bloqueo

La palabra clave lock 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 comienza con la palabra clave lock, que utiliza un objeto como argumento, seguida de un bloque de código que sólo un subproceso puede ejecutar a la vez. Por ejemplo:

public class TestThreading
{
    private System.Object lockThis = new System.Object();

    public void Function()
    {

        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 a lock sólo 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 la palabra clave lock, vea:

Monitores

Al igual que la palabra clave lock, 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. De hecho, la palabra clave lock se implementa con la clase Monitor. Por ejemplo:

lock (x)
{
    DoSomething();
}

Esto equivale a:

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 en vez de utilizar directamente la clase Monitor, porque lock es más conciso y 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.

Para obtener más información sobre los monitores, vea Ejemplo Monitor Synchronization Technology.

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 a que se produzcan eventos si se llama a uno de los métodos de espera, como WaitOne, WaitAny o WaitAll. 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.

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();
    }
}

Para obtener más ejemplos del uso de eventos de sincronización de subprocesos, vea:

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).

Secciones relacionadas

Vea también

Conceptos

Guía de programación de C#

Referencia

Thread

WaitOne

WaitAny

WaitAll

Monitor

Mutex

AutoResetEvent

ManualResetEvent

Interlocked

WaitHandle

Otros recursos

Implementación del modelo de programación asincrónica de CLR

APM simplificado con C#

Monitor de interbloqueo