Compartir a través de


Problemas de sincronización y multiprocesador

Las aplicaciones pueden surgir problemas al ejecutarse en sistemas multiprocesador debido a suposiciones que hacen que solo son válidas en sistemas de un solo procesador.

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 prioridad más alta no renunciará al control de prioridad inferior al subproceso de prioridad inferior, ya que el programador da preferencia a 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 carrera. 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 de varios procesadores.

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 desordenados. 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 de que la instrucción de barrera de memoria se confirmen en la memoria antes de cualquier operación de lectura y escritura de memoria que aparezca después de la instrucción de barrera de memoria. Una barrera de memoria de lectura ordena solo las operaciones de lectura de memoria y una barrera de memoria de escritura ordena solo 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 las barreras de memoria con la 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 versión, los resultados de la operación están disponibles después de los resultados de cualquier operación que aparezca antes que en el código. La semántica de barrera combina la semántica de adquisición y versió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 los procesadores x86 y x64 que admiten SSE2, las instrucciones son mfence (barrera de memoria), lfence (barrera de carga) y sfence (valla de almacén). En los procesadores ARM, las instruciones se dmb y dsb. Para obtener 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 o dejan secciones críticas
  • 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
  • Funciones interbloqueadas (excepto funciones con noFence sufijo o intrínsecos con _nf sufijo)

Corrección de una condición de carrera

El código siguiente tiene una condición de carrera 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 carrera anterior se puede reparar mediante la palabra clave volátil o la funciónInterlockedExchangepara 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 de y semántica de versión para las operaciones de escritura en variables de 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, se ordenan volátiles para referencias de volátiles; el compilador no volverá a ordenar 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íticos

de acceso a variables interbloqueadas

funciones de espera