Compartir a través de


Problemas de sincronización y multiprocesador

Las aplicaciones pueden tener problemas cuando se ejecutan en sistemas multiprocesador debido a suposiciones que solo son válidas en sistemas monoprocesador.

Prioridades de subprocesos

Considere un programa con dos subprocesos, uno con una prioridad más alta que la otra. En un sistema de un solo procesador, el subproceso de mayor prioridad no cederá el control al de menor prioridad porque el planificador da preferencia a los subprocesos de mayor prioridad. En un sistema multiprocesador, ambos subprocesos se pueden ejecutar simultáneamente, cada uno en su propio procesador.

Las aplicaciones deben sincronizar el acceso a las estructuras de datos para evitar condiciones de anticipación. El código que supone que los subprocesos de mayor prioridad se ejecutan sin interferencias de subprocesos de prioridad inferior producirán un error en los sistemas multiprocesador.

Ordenación de memoria

Cuando un procesador escribe en una ubicación de memoria, el valor se almacena en caché para mejorar el rendimiento. Del mismo modo, el procesador intenta satisfacer las solicitudes de lectura de la memoria caché para mejorar el rendimiento. Además, los procesadores comienzan a capturar valores de la memoria antes de que la aplicación los solicite. Esto puede ocurrir como parte de la ejecución especulativa o debido a problemas de línea de caché.

Las cachés de CPU se pueden dividir en bancos a los que se puede acceder en paralelo. Esto significa que las operaciones de memoria se pueden completar desordenadas. Para asegurarse de que las operaciones de memoria se completan en orden, la mayoría de los procesadores proporcionan instrucciones de barrera de memoria. Una barrera de memoria completa garantiza que las operaciones de lectura y escritura de memoria que aparecen antes que la instrucción de barrera de memoria se confirmen en la memoria antes que las operaciones de lectura y escritura de memoria que aparecen después de la instrucción de barrera de memoria. Una barrera de memoria de lectura solo ordena las operaciones de lectura de memoria y una barrera de memoria de escritura solo ordena las operaciones de escritura de memoria. Estas instrucciones también garantizan que el compilador deshabilite las optimizaciones que podrían reordenar las operaciones de memoria a través de las barreras.

Los procesadores pueden admitir instrucciones para limitaciones de memoria con semántica de adquisición, liberación y barrera. Esta semántica describe el orden en el que los resultados de una operación están disponibles. Con la semántica de adquisición, los resultados de la operación están disponibles antes de los resultados de cualquier operación que aparezca después de ella en el código. Con la semántica de liberación, los resultados de la operación están disponibles después de los resultados de cualquier operación que aparezca antes de ella en el código. La semántica de limitación combina la semántica de adquisición y liberación. Los resultados de una operación con semántica de barrera están disponibles antes de las de cualquier operación que aparezca después de ella en el código y después de las de cualquier operación que aparezca antes de ella.

En procesadores x86 y x64 que admiten SSE2, las instrucciones son mfence (barrera de memoria), lfence (barrera de carga) y sfence (barrera de almacenamiento). En los procesadores ARM, las instruciones son dmb y dsb. Para más información, consulte la documentación del procesador.

Las siguientes funciones de sincronización usan las barreras adecuadas para garantizar el orden de memoria:

  • Funciones que entran en secciones críticas o salen de ellas.
  • Funciones que adquieren o liberan bloqueos SRW.
  • Inicio y finalización de inicialización única.
  • Función EnterSynchronizationBarrier.
  • Funciones que indican objetos de sincronización.
  • Funciones de espera (wait).
  • Funciones interbloqueadas (excepto funciones con sufijo NoFence o intrínsecas con sufijo _nf).

Corrección de una condición de anticipación

El código siguiente tiene una condición de anticipación en un sistema multiprocesador porque el procesador que ejecuta CacheComputedValue la primera vez puede escribir fValueHasBeenComputed en la memoria principal antes de escribir iValue en la memoria principal. Por lo tanto, un segundo procesador que ejecuta FetchComputedValue al mismo tiempo lee fValueHasBeenComputed como TRUE, pero el nuevo valor de iValue sigue en la memoria caché del primer procesador y no se ha escrito en la memoria.

int iValue;
BOOL fValueHasBeenComputed = FALSE;
extern int ComputeValue();

void CacheComputedValue()
{
  if (!fValueHasBeenComputed) 
  {
    iValue = ComputeValue();
    fValueHasBeenComputed = TRUE;
  }
}
 
BOOL FetchComputedValue(int *piResult)
{
  if (fValueHasBeenComputed) 
  {
    *piResult = iValue;
    return TRUE;
  } 

  else return FALSE;
}

Esta condición de anticipación se puede reparar mediante la palabra clave volatile o la función InterlockedExchange para asegurarse de que el valor de iValue se actualiza para todos los procesadores antes de que el valor de fValueHasBeenComputed esté establecido en TRUE.

A partir de Visual Studio 2005, si se compila en modo /volatile:ms, el compilador usa la semántica de adquisición para las operaciones de lectura en variables volátiles y semántica de liberación para las operaciones de escritura en variables volátiles (cuando se admite la CPU). Por lo tanto, puede corregir el ejemplo de la siguiente manera:

volatile int iValue;
volatile BOOL fValueHasBeenComputed = FALSE;
extern int ComputeValue();

void CacheComputedValue()
{
  if (!fValueHasBeenComputed) 
  {
    iValue = ComputeValue();
    fValueHasBeenComputed = TRUE;
  }
}
 
BOOL FetchComputedValue(int *piResult)
{
  if (fValueHasBeenComputed) 
  {
    *piResult = iValue;
    return TRUE;
  } 

  else return FALSE;
}

Con Visual Studio 2003, las referencias volátiles a volátiles están ordenadas; el compilador no reordenará el acceso a variables volátiles. Sin embargo, el procesador podría volver a ordenar estas operaciones. Por lo tanto, puede corregir el ejemplo de la siguiente manera:

int iValue;
BOOL fValueHasBeenComputed = FALSE;
extern int ComputeValue();

void CacheComputedValue()
{
  if (InterlockedCompareExchange((LONG*)&fValueHasBeenComputed, 
          FALSE, FALSE)==FALSE) 
  {
    InterlockedExchange ((LONG*)&iValue, (LONG)ComputeValue());
    InterlockedExchange ((LONG*)&fValueHasBeenComputed, TRUE);
  }
}
 
BOOL FetchComputedValue(int *piResult)
{
  if (InterlockedCompareExchange((LONG*)&fValueHasBeenComputed, 
          TRUE, TRUE)==TRUE) 
  {
    InterlockedExchange((LONG*)piResult, (LONG)iValue);
    return TRUE;
  } 

  else return FALSE;
}

Objetos de sección crítica

Acceso a variables interbloqueadas

Funciones de espera (wait)