实现线程锁时是否真的需要原子性

Is atomicity really needed when implementing a thread lock?

本文关键字:原子性 真的 是否 线程 实现      更新时间:2023-10-16

假设我有一段代码不能同时执行。我尝试线程锁的(假定是天真的)方法看起来是这样的:

int lock = 0;
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
    if (lock)
        return 1;
    lock = 1;
    /* code goes here */
    lock = 0;
    return 0;
}

当使用以下内容进行测试时:

for (i = 0; i < 2; i++)
    thandle[i] = CreateThread(NULL, 0, ThreadProc, NULL, 0, &tid[i]);
WaitForMultipleObjects(2, thandle, TRUE, INFINITE);

我总是得到所需的效果,即只有要创建的第一个线程才能真正到达代码并返回0。然而,我不断遇到这样的建议,即这种方法可能会失败,因为锁不是使用原子操作实现的。

我看不到同时创建线程的方法,所以,从实际角度来看,这里真的需要原子性吗?有人能提供一个导致上述失败的例子吗?

您需要能够同时原子地检查锁和获取锁。

可能出现以下情况:

  1. 线程1检查锁定:

    if (lock) // lock = 0 so skips the body of the if statement
    
  2. 线程2同时检查锁定:

    if (lock) // lock = 0 so skips the body of the if statement
    
  3. 线程1分配锁:

    lock = 1
    
  4. 线程2分配锁:

    lock = 1
    
  5. 线程1运行其代码:

    /* code goes here */ // Thread 1 starts running the critical section code
    
  6. 线程2同时运行其代码:

    /* code goes here */ // Thread 2 starts running the critical section code
    

由于没有使用原子"测试和获取锁",在线程1检查锁和设置锁之间,线程2能够检查锁,因此两个线程可以同时处于关键部分。

在单核系统上,如果在线程1检查锁和设置锁之间发生到线程2的任务切换,就会发生这种情况。

在多核系统上,如果两个线程都在同一个核心上,并且任务切换到线程2,就会发生这种情况;如果线程在不同的核心上运行,也会发生这种情况。

是的,我知道这怎么会失败。

如果你在单核/单处理器机器上运行它,你可能会永远运行它,永远不会出现故障。

在一台有多个核心的机器上,看到故障可能是家常便饭。

如果机器上的所有核心都很忙,它就会失败。创建新线程的一个线程将创建多个线程,但没有一个线程立即开始运行,因为核心都很忙。

然后,许多其他线程同时完成处理,并且您的一些线程都同时开始。它们现在都在锁步骤中执行,所以它们都读取相同的值,都试图写入相同的值(产生未定义的行为),都同时执行代码,都将锁设置为0,并且都返回0。

除非你有很多线程和核心,否则在给定的运行中发生这种情况的几率可能相当低。事实上,这是有疑问的,它最终会发生——涉及的线程和内核越多,发生的时间就越早、频率就越高。

大多数时候,您的代码都是可以的,但在某些情况下,它会失败,两个或多个线程可能会同时执行受锁保护的代码。

想象两个线程同时开始执行您的函数。他们都将执行

if (lock)
    return 1;

在到达之前

lock = 1;

从而两者都进入受保护的代码。这就是为什么锁必须是原子锁。

该解决方案非常简单,只需使用Win32函数InitializeCriticalSection创建一个关键节,并将其与EnterCriticalSection和LeaveCriticalSection一起使用即可。

您的代码充满了问题:

  • lock不是volatile,因此编译器可以自由地将其读取到寄存器中,并在寄存器中继续使用/更新它,而无需将更改写回内存(在内存中,至少有机会引起其他线程的注意,尽管在许多CPU上,需要显式内存屏障(同步操作码)来保证其他线程的可见性)

    • 请注意,当您使用适当的同步机制(互斥或原子操作)时,也不需要使用volatile
  • 指令可能会被重新排序,以便在您尝试锁定之前或尝试解锁之后执行一些/* code [that] goes here */

  • CCD_ 5测试与CCD_。。。即使线程正在写入一个原子变量,您也需要一个Compare and Swap/Compare and Exchange风格的操作来保证它的安全性,如果失败了,您需要旋转等待-燃烧CPU-或者在多次尝试后屈服,然后重试(你猜怎么着-到那时你已经重新实现了互斥,而不是使用系统的互斥)

由于代码使用的是像CreateThread()、WaitForMultipleObjects()这样的Windows函数,所以不妨使用Windows互斥或信号量,并在线程中使用WaitForSingleObject(),假设您希望线程等待而不是中止。