Windows Via C/C++:用户模式下的线程同步——原子操作:Interlocked函数族

原子操作在线程同步中的地位非常重要,它保证了当线程访问某资源时其它线程无法在同一时刻访问该资源。以下面的代码为例:

// Define a global variable
long g_x = 0;

DWORD WINAPI ThreadFunc1(PVOID pvParam)
{
  g_x ++;
  return 0;
}

DWORD WINAPI ThreadFunc2(PVOID pvParam)
{
  g_x ++;
  return 0;
}

g_x被声明为全局变量并初始化为0,现在假如我创建了两个线程,一个执行ThreadFunc1,另一个执行ThreadFunc2。ThreadFunc1和ThreadFunc2的功能都是将g_x加1。你可能会想ThreadFunc1和ThreadFunc2返回后,g_x的值应该是2,然而这只是所有可能结果中的一个,你无法预测g_x的最终值。编译器会为g_x++产生类似下面的汇编代码:
MOV EAX, [g_x] ; move the value in g_x into a register

INC EAX ; increase the value in the register

MOV [g_x], EAX ; store the new value back into g_x

两个线程并发执行时,可能会产生下面的代码序列:
MOV EAX, [g_x] ; thread 1: move 0 into a register

INC EAX ; thread 1: increase the register to 1

MOV EAX, [g_x] ; thread 2: move 0 into the register again

MOV [g_x], EAX ; thread 1: move 0 into g_x

INC EAX ; thread 2: increase the register to 1

MOV [g_x], EAX ; thread 2: move 1 into g_x

上面代码执行后,g_x的最终值是1!这个结果让人惊讶,开发人员根本无法干预系统调度程序的行为。事实上,就算有100个线程同时执行g_x++,g_x的最终值有可能还是1!这与我们的期望相去甚远,我们希望0自增两次的结果一直是2,而不会被编译器产生的代码影响、不会被CPU调度线程的行为影响、不会被计算机系统安装的CPU的数量影响。幸运的是,Windows提供了一些函数可以保证我们的代码产生预期的、正确的结果。为了解决这个问题,我们必须保证自增操作是原子的——也就是说,它不会被其它线程中断。Windows提供的interlocked函数族为此提供了解决方案。Interlocked函数族相当简单且易于理解,所有的interlocked函数对参数的操作都是原子的。比如下面的InterlockedExchangeAdd和InterlockedExchangeAdd64函数:

LONG InterlockedExchangeAdd(
  PLONG volatile plAddend,
  LONG lIncrement);
LONGLONG InterlockedExchangeAdd64(
  PLONGLONG volatile pllAddend,
  LONGLONG lIncrement);

我们可以用上面的函数重写g_x++操作:

long g_x = 0;

DWORD WINAPI ThreadFunc1(PVOID pvParam){
  InterlockedExchangeAdd(&g_x, 1);
  return 0;
}

DWORD WINAPI ThreadFunc2(PVOID pvParam){
  InterlockedExchangeAdd(&g_x, 1);
  return 0;
}

现在,g_x的自增是原子的,其最终值保证是2。注如果你只需要将某值增加1的话,可以使用InterlockedIncrement函数。所有试图修改共享变量的线程都应该调用Interlocked函数族代替简单的C++算术运算:

// the long variable shared by many threads
LONG g_x;

// Incorrect way to increase the variable
g_x ++;

// Correct way to increase the variable
InterlockedIncrement(&g_x);

Interlocked函数族的实现细节取决于当前CPU平台。在x86架构的CPU上,Interlocked函数会在总线上设置硬件信号以阻止其它线程访问被锁定的内存区域。Interlocked函数族的实现细节并不重要,重要的是它们保证了对参数的操作是原子的,不受编译器和CPU架构、数量的影响。要注意传递给Interlocked函数族的变量地址必须按32位/64位对齐,C运行时提供了_aligned_malloc和aligned_realloc函数来创建/重新分配按指定位数对齐的内存块:void * _aligned_malloc(size_t size, size_t alignment),size表示要分配的字节数,alignment是该对齐该内存块的字节边界,alignment必须是2的倍数。此外,Interlocked函数族的执行速度相当快,通常不超过50个CPU周期,且无需在用户模式和内核模式间转换(执行这种转换通常所需的CPU周期数通常大于1000)。

下面是三个用来替换指定值的interlocked函数:

LONG InterlockedExchange(
  PLONG volatile plTarget,
  LONG lValue);

LONGLONG InterlockedExchange64(
  PLONGLONG volatile plTarget,
  LONGLONG lValue);

PVOID InterlockedExchangePointer(
  PVOID* volatile ppvTarget,
  PVOID pvValue);

函数InterlockedExchange和InterlockedExchangePointer用第二个参数的值替代第一个参数地址中的值,替换过程是原子操作。在32位应用程序中,函数将用32位的值替换32位的值,在64位的应用程序中,InterlockedExchange仍然使用32值,但InterlockedExchangePointer使用64位值。函数均返回第一个参数地址中的旧值。InterlockedExchange在编写自旋锁时非常有用:

// Global variable indicating whether a shared resource is in user or not
BOOL g_fResourceInUser = FALSE;

void Func1() {
  // wait to access the resource
  while(InterlockedExchange(&g_fResourceInUse, TRUE) == TRUE)
    Sleep(0);
  // access the resource
  ...
  // reset the flag
  InterlockedExchange(&g_fResourceInUse, FALSE);
}

上面代码含义比较清晰,不再多做解释。要注意自旋锁会浪费CPU时间。在自旋锁中,CPU必须不断比较两个值是决定是否跳出循环。上面代码要求所有使用自旋锁的线程优先级必须相同,否则若位于自旋锁内的线程拥有较高优先级,CPU将一直陷入自旋锁的循环中,此外,对使用自旋锁的线程应该调用SetProcessPriorityBoost/SetThreadPriorityBoost禁止系统提升其优先级。

使用自旋锁时,线程访问受保护资源的时间应该尽可能的短。更为有效的方法是首先使用自旋锁等待,经过一段时间后若依然无法访问受保护的资源,则转入内核模式等待(此时线程将被挂起而不会消耗CPU时间)直到资源被其它线程释放,这就是临界区(Critical Section)的实现原理。

下面是最后两个交换Interlocked值替换函数:

LONG InterlockedCompareExchange(
  PLONG plDestination,
  LONG lExchange,
  LONG lCompared);

PVOID InterlockedCompareExchangePointer(
  PVOID* ppvDestination,
  PVOID pvExchange,
  PVOID pvCompared);

函数将当前值(plDestination/ppvDestination)和lCompared/pvCompared相比较,二者相等时,当前值将被lExchange/pvExchange替换,否则当前值保持不变,函数返回当前值的旧值。

除了上面讨论的,Windows还提供了一些Interlocked函数,不过这些函数都是用上面的函数实现的,比如:

LONG InterlockedIncrement(PLONG plAddend);

LONG InterlockedDecrement(PLONG plAddend);

这两个函数显然可以由InterlockedExchangeAdd实现。除此之外,还有基于InterlockedCompareExchange64实现的用于OR、XOR和AND操作的Interlocked函数,比如InterlockedAnd64:

LONGLONG InterlockedAnd64(LONGLONG* Destination, LONGLONG value) {
  LONGLONG old = *Destination;
  do {
    old = *Destination;
  } while(InterlockedCompareExchange64(Destination, old&value, old) != old);
  return old;
}

不明白为什么不直接用下面的方式写呢:
LONGLONG InterlockedAnd64(LONGLONG* Destination, LONGLONG value) {
  return InterlockedCompareExchange64(Destination, (*Destination)&value, *Destination);
}

从Windows XP开始,除了对整数和布尔值的原子操作,开发人员可以使用新的函数操作一种被称为“Interlocked Singly Linked List”的堆栈。在该栈上的每一种操作,如压栈、弹出等都是原子操作,下表列出了这些函数:

函数名 描述
InitializeSListHead Creates an empty stack
InterlockedPushEntrySList Adds an element on top of the stack
InterlockedPopEntrySList Removes the top element of the stack and returns it
InterlockedFlushSList Empties the stack
QueryDepthSList Returns the number of elements stored in the stack

你可能感兴趣的:(Windows Via C/C++:用户模式下的线程同步——原子操作:Interlocked函数族)