Procedimientos recomendados para el subprocesamiento administrado

El multithreading requiere que la programación sea cuidadosa. La complejidad de muchas tareas se puede reducir poniendo las solicitudes de ejecución en cola por subprocesos del grupo de subprocesos. En este tema se tratan situaciones más complicadas, como coordinar el trabajo de múltiples subprocesos, o controlar los subprocesos que se bloquean.

Nota:

A partir de .NET Framework 4, la biblioteca TPL y PLINQ proporcionan API que reducen parte de la complejidad y los riesgos de la programación multiproceso. Para más información, consulte Programación en paralelo en .NET.

Interbloqueos y condiciones de carrera

El multithreading resuelve problemas de rendimiento y de capacidad de respuesta, pero al hacerlo también crea nuevos problemas, como interbloqueos y condiciones de carrera.

Interbloqueos

Un interbloqueo tiene lugar cuando dos subprocesos intentan bloquear un recurso que ya ha bloqueado uno de estos subprocesos. Ninguno de los subprocesos puede avanzar.

Muchos métodos de las clases del subprocesamiento administrado ofrecen tiempos de espera que se utilizan para detectar interbloqueos. Por ejemplo, con el siguiente código se intenta obtener un bloqueo en un objeto llamado lockObject. Si el bloqueo no se consigue en 300 milisegundos, Monitor.TryEnter devuelve el valor false.

If Monitor.TryEnter(lockObject, 300) Then  
    Try  
        ' Place code protected by the Monitor here.  
    Finally  
        Monitor.Exit(lockObject)  
    End Try  
Else  
    ' Code to execute if the attempt times out.  
End If  
if (Monitor.TryEnter(lockObject, 300)) {  
    try {  
        // Place code protected by the Monitor here.  
    }  
    finally {  
        Monitor.Exit(lockObject);  
    }  
}  
else {  
    // Code to execute if the attempt times out.  
}  

Condiciones de carrera

Una condición de carrera es un error que se produce cuando el resultado de un programa depende del primero de dos o más subprocesos que consiga llegar hasta un bloque específico de código. Ejecutar el programa muchas veces genera distintos resultados y no es posible predecir el resultado de una ejecución específica.

Un ejemplo sencillo de una condición de carrera es el incremento de un campo. Suponga una clase que tiene un campo privado static (Shared en Visual Basic) que se incrementa cada vez que se crea una instancia de la clase, mediante código como objCt++; (C#) o objCt += 1 (Visual Basic). Esta operación requiere cargar el valor de objCt en un registro, incrementar el valor y almacenarlo en objCt.

En una aplicación multiproceso, un subproceso que realiza los tres pasos puede adelantar al subproceso que ha cargado e incrementado el valor; cuando el primer subproceso reanuda la ejecución y almacena su valor, sobrescribe objCt sin tener en cuenta el hecho de que el valor ha cambiado mientras tanto.

Para evitar fácilmente esta condición de carrera determinada, utilice los métodos de la clase Interlocked, como Interlocked.Increment. Para más información sobre otras técnicas de sincronización de datos entre varios subprocesos, consulte Sincronización de datos para multithreading.

También se pueden producir condiciones de carrera al sincronizar las actividades de varios subprocesos. Siempre que escriba una línea de código, debe tener en cuenta qué puede ocurrir si otro subproceso adelanta a un subproceso antes de ejecutar la línea (o antes de cualquiera de las instrucciones máquina que forman la línea).

Miembros estáticos y constructores estáticos

No se inicializa una clase hasta que su constructor de clase (constructorstatic en C#, Shared Sub New en Visual Basic) haya terminado de ejecutarse. Para evitar la ejecución de código en un tipo no inicializado, Common Language Runtime bloquea todas las llamadas de otros subprocesos a los miembros static de la clase (miembrosShared en Visual Basic) hasta que el constructor de clase termina de ejecutarse.

Por ejemplo, si un constructor de clase inicia un nuevo subproceso, y el procedimiento del subproceso llama a un miembro static de la clase, el nuevo subproceso se bloquea hasta que el constructor de clase finalice.

Esto se aplica a cualquier tipo que pueda tener un constructor static.

Número de procesadores

Si hay varios procesadores o uno solo disponibles en un sistema puede influir en la arquitectura de multiproceso. Para obtener más información, vea Número de procesadores.

Use la propiedad Environment.ProcessorCount para determinar el número de procesadores disponibles en tiempo de ejecución.

Recomendaciones generales

Tenga en cuenta las siguientes instrucciones cuando utilice varios subprocesos:

  • No utilice Thread.Abort para finalizar otros subprocesos. Una llamada a Abort en otro subproceso es similar a iniciar una excepción en ese subproceso, sin conocer qué punto ha alcanzado en su procesamiento.

  • No utilice Thread.Suspend ni Thread.Resume para sincronizar las actividades de varios subprocesos. Utilice Mutex, ManualResetEvent, AutoResetEvent y Monitor.

  • No controle la ejecución de subprocesos de trabajo desde el programa principal (con eventos, por ejemplo). En su lugar, diseñe un programa de forma que los subprocesos de trabajo sean los que tengan que esperar hasta que haya trabajo disponible, lo ejecuten y notifiquen su finalización a otras partes del programa. Si los subprocesos de trabajo no se bloquean, puede ser conveniente usar subprocesos del grupo de subprocesos. Monitor.PulseAll resulta útil en aquellas situaciones en las que los subprocesos de trabajo se bloquean.

  • No utilice los tipos como objetos de bloqueo. Es decir, evite código como lock(typeof(X)) en C# o SyncLock(GetType(X)) en Visual Basic o el uso de Monitor.Enter con objetos Type. Para un tipo determinado, hay sólo una instancia de System.Type por el dominio de aplicación. Si el tipo que quiere bloquear es público, codifique uno que su tipo no pueda bloquear, para evitar interbloqueos. Para otros problemas, consulte Procedimientos recomendados para la confiabilidad.

  • Tenga cuidado al efectuar bloqueos en instancias, por ejemplo lock(this) en C# o SyncLock(Me) en Visual Basic. Si otra parte del código de la aplicación, ajeno al tipo, bloquea el objeto, podrían producirse interbloqueos.

  • Asegúrese de que un subproceso que entra en un monitor siempre sale de ese monitor, aun en el caso de que se produzca una excepción mientras el subproceso se encuentra en el monitor. La instrucción lock de C# y la instrucción SyncLock de Visual Basic ofrecen automáticamente este comportamiento mediante un bloque finally que garantiza la llamada a Monitor.Exit. Si no está seguro de que se llamará a Exit, considere la posibilidad de cambiar el diseño con el fin de usar Mutex. Una zona de exclusión mutua se libera automáticamente cuando finaliza el subproceso al que pertenece.

  • Utilice varios subprocesos para tareas que requieren recursos diferentes, y evite asignar varios subprocesos a un solo recurso. Por ejemplo, en tareas que impliquen beneficios de E/S por tener un subproceso propio, ya que ese subproceso se bloquea durante las operaciones de E/S y, de este modo, permite ejecutar otros subprocesos. Los datos proporcionados por el usuario son otro recurso que se beneficia de la utilización de un subproceso dedicado. En un equipo de un solo procesador, una tarea que implica un cálculo intensivo coexiste con los datos proporcionados por el usuario y con tareas que implican la E/S, pero varias tareas de cálculo intensivo compiten entre ellas.

  • Considere la posibilidad de utilizar métodos de la clase Interlocked para los cambios de estado simples, en lugar de utilizar la instrucción lock (SyncLock en Visual Basic). La instrucción lock es una buena herramienta de uso general, pero la clase Interlocked genera mejor rendimiento para las actualizaciones que deben ser atómicas. Internamente, ejecuta un solo prefijo de bloqueo si no hay contención. En las revisiones de código, inspeccione código similar al que se muestra en los ejemplos siguientes. En el primer ejemplo, se incrementa una variable de estado:

    SyncLock lockObject  
        myField += 1  
    End SyncLock  
    
    lock(lockObject)
    {  
        myField++;  
    }  
    

    Para mejorar el rendimiento, utilice el método Increment en lugar de la instrucción lock, de la siguiente manera:

    System.Threading.Interlocked.Increment(myField)  
    
    System.Threading.Interlocked.Increment(myField);  
    

    Nota:

    Use el método Add para incrementos atómicos mayores que 1.

    En el segundo ejemplo, se actualiza una variable de tipo de referencia sólo si es una referencia nula (Nothing en Visual Basic).

    If x Is Nothing Then  
        SyncLock lockObject  
            If x Is Nothing Then  
                x = y  
            End If  
        End SyncLock  
    End If  
    
    if (x == null)  
    {  
        lock (lockObject)  
        {  
            x ??= y;
        }  
    }  
    

    Se puede mejorar el rendimiento utilizando el método CompareExchange de la siguiente manera:

    System.Threading.Interlocked.CompareExchange(x, y, Nothing)  
    
    System.Threading.Interlocked.CompareExchange(ref x, y, null);  
    

    Nota

    El método CompareExchange<T>(T, T, T) proporciona una alternativa con seguridad de tipos para tipos de referencia.

Recomendaciones para las bibliotecas de clases

Tenga en cuenta las instrucciones siguientes cuando diseñe bibliotecas de clases para el multithreading:

  • Evite la necesidad de sincronización si es posible. Esto se aplica especialmente en el caso de código muy utilizado. Por ejemplo, se podría ajustar un algoritmo de modo que tolere una condición de carrera en lugar de eliminarla. La sincronización innecesaria disminuye el rendimiento y crea la posibilidad de interbloqueos y condiciones de carrera.

  • Procure que los datos estáticos (Shared en Visual Basic) sean seguros para subprocesos de manera predeterminada.

  • No convierta los datos de instancia en datos seguros para subprocesos de manera predeterminada. Al agregar bloqueos para crear código seguro para subprocesos, se reduce el rendimiento, se incrementa la contención de bloqueos y se crea la posibilidad de que se produzcan interbloqueos. En los modelos de aplicación comunes, sólo un subproceso a la vez ejecuta código de usuario, lo que minimiza la necesidad de la seguridad para subprocesos. Por esta razón, las bibliotecas de clases de .NET no son seguras para subprocesos de forma predeterminada.

  • Evite proporcionar métodos estáticos que alteren el estado estático. En escenarios de servidor comunes, el estado estático se comparte entre las solicitudes, lo que significa que varios subprocesos pueden ejecutar a la vez ese código. De este modo, se abre la posibilidad de errores de subprocesos. Considere la posibilidad de utilizar un modelo de diseño que encapsule los datos en instancias no compartidas por las solicitudes. Además, si se sincronizan los datos estáticos, las llamadas entre los métodos estáticos que modifican el estado pueden generar interbloqueos o sincronización redundante, lo que afecta negativamente al rendimiento.

Vea también