同步和多处理器问题

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

线程优先级

想象一下具有两个线程的程序,一个线程优先级高于另一个线程。 在单处理器系统上,高优先级线程不会将控制权让与低优先级线程,因为计划程序会将优先权赋予优先级较高的线程。 在多处理器系统上,两个线程都可以同时运行,每个线程在其自己的处理器上运行。

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

内存排序

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

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

处理器可以支持具有获取、释放和围栏语义的内存屏障指令。 这些语义描述了操作结果变得可用的顺序。 使用获取语义,该操作的结果会在代码中后出现的任何操作的结果之前可用。 使用释放语义,该操作的结果会在代码中先出现的任何操作的结果之后可用。 围栏语义结合了获取和释放语义。 具有围栏语义的操作的结果在代码中后出现的任何操作的结果之前以及先出现的任何操作的结果之后可用。

在支持 SSE2 的 x86 和 x64 处理器上,指令是 mfence(内存围栏)、lfence(负载围栏)和 sfence(存储围栏)。 在 ARM 处理器上,指令是 dmb 和 dsb。 有关详细信息,请参阅处理器的相关文档。

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

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

修复争用条件

以下代码在多处理器系统上存在争用条件,因为首次执行 CacheComputedValue 的处理器可能会在将 iValue 写入主内存之前将 fValueHasBeenComputed 写入主内存。 因此,同时执行 FetchComputedValue 的第二个处理器会将 fValueHasBeenComputed 读取为 TRUE,但 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;
}

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

从 Visual Studio 2005 开始,如果在 /volatile:ms 模式下编译,编译器将对 volatile 变量上的读取操作使用获取语义,对 volatile 变量上的写入操作使用释放语义(如果 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 时,volatile 到 volatile 引用会进行排序;编译器不会将 volatile 变量访问重新排序。 但是,处理器可以将这些操作重新排序。 因此,可以按如下所示更正示例:

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

Critical Section 对象

互锁变量访问

等待函数