同步和多处理器问题

应用程序在多处理器系统上运行时可能会遇到问题,因为假设它们仅在单处理器系统上有效。

线程优先级

请考虑具有两个线程的程序,一个线程优先级高于另一个线程。 在单处理器系统上,高优先级线程不会放弃对较低优先级线程的控制,因为计划程序优先于优先级较高的线程。 在多处理器系统上,两个线程都可以同时运行,每个线程在其自己的处理器上运行。

应用程序应同步对数据结构的访问,以避免争用情况。 假定高优先级线程在多处理器系统上运行且不受较低优先级线程干扰的代码将失败。

内存排序

当处理器写入内存位置时,将缓存该值以提高性能。 同样,处理器会尝试满足缓存中的读取请求以提高性能。 此外,处理器开始从内存中提取值,然后再由应用程序请求这些值。 这可能是推理执行的一部分,也可能是由于缓存行问题造成的。

CPU 缓存可以分区为可以并行访问的银行。 这意味着内存作可以无序完成。 为了确保内存作按顺序完成,大多数处理器都提供内存屏障指令。 完整内存屏障 可确保内存读取和写入作在内存屏障指令提交到内存之前,内存屏障指令之后出现的任何内存读取和写入作。 读取内存屏障 只对内存读取作进行排序,写入内存屏障 只对内存写入作进行排序。 这些说明还确保编译器禁用任何优化,这些优化可能会对屏障上的内存作重新排序。

处理器可以支持有关具有获取、释放和围栏语义的内存屏障的说明。 这些语义描述了作结果可用的顺序。 通过获取语义,作的结果在代码中出现的任何作的结果之前可用。 使用发布语义时,作的结果在代码中出现的任何作的结果后可用。 围栏语义结合了获取和释放语义。 具有围栏语义的作的结果在代码中出现的任何作之前以及之前出现的任何作的结果可用。

在支持 SSE2 的 x86 和 x64 处理器上,指令 (内存围栏)、(负载围栏)和 围栏(存储围栏)。 在 ARM 处理器上,入侵 dmbdsb。 有关详细信息,请参阅处理器的文档。

以下同步函数使用适当的屏障来确保内存排序:

  • 输入或离开关键节的函数
  • 获取或释放 SRW 锁的函数
  • 一次性初始化开始和完成
  • EnterSynchronizationBarrier 函数
  • 信号同步对象的函数
  • 等待函数
  • 互锁函数(除具有 NoFence 后缀的函数或带有 _nf 后缀的内部函数除外)

修复争用条件

以下代码在多处理器系统上具有争用条件,因为第一次执行 CacheComputedValue 的处理器可能会在将 iValue 写入主内存之前将 fValueHasBeenComputed 写入主内存。 因此,执行 FetchComputedValue 的第二个处理器同时读取 TRUEfValueHasBeenComputed,但 iValue 的新值仍在第一个处理器的缓存中,并且尚未写入内存。

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

可以使用 易失性 关键字或 InterlockedExchange 函数修复上述争用条件,以确保在将 fValueHasBeenComputed 值设置为 true之前,为所有处理器更新 iValue 的值。

从 Visual Studio 2005 开始,如果在 /volatile:ms 模式下编译,编译器将使用获取语义来读取 可变 变量的语义,并为 可变 变量(CPU 支持)上的写入作释放语义。 因此,可以按如下所示更正示例:

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

使用 Visual Studio 2003,将 可变可变 引用进行排序;编译器不会 可变 变量访问重新排序。 但是,处理器可以重新排序这些作。 因此,可以按如下所示更正示例:

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

关键节对象

互锁变量访问

等待函数