实现线程锁时真的需要原子性吗?

时间:2015-03-05 00:04:22

标签: c++ c windows multithreading

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

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.但是,我不断遇到这个方法可能失败的建议,因为没有使用原子操作实现锁。 / p>

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

5 个答案:

答案 0 :(得分:4)

您需要能够以原子方式检查锁定并同时获取锁定。

可能发生以下情况:

  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
    
  7. 由于没有使用原子“test and acquire lock”,在线程1检查锁定并设置锁定的时间之间,线程2能够检查锁定,因此两个线程都可以在关键部分在同一时间。

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

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

答案 1 :(得分:3)

是的,我可以看到这可能会失败。

如果您在单核/单处理器计算机上运行它,您可以永远运行它,并且永远不会看到故障。

在具有多个内核的计算机上,看到故障可能是相当常规的。

如果机器上的所有核心都忙,那么失败的显而易见的方法就是。创建新线程的一个线程将创建许多线程,其中没有一个线程立即开始运行,因为核心都很忙。

然后许多其他线程同时完成处理,并且许多线程都同时启动。所有这些都在锁定步骤中执行,因此它们都读取相同的值,所有尝试写入相同的值(产生未定义的行为),所有都同时执行代码,所有设置锁定为0,并且全部返回0。

除非你有很多线程和核心,否则在给定的运行中发生这种情况的可能性可能相当低。毫无疑问,它最终能够并且将会发生 - 并且涉及的线程和内核越多,它就会越早发生。

答案 2 :(得分:3)

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

两个线程在同一时间开始执行函数的图像。他们都会执行

if (lock)
    return 1;
到达之前

lock = 1;

因此都输入受保护的代码。这就是为什么锁必须是原子的。

解决方案非常简单,只需使用Win32函数InitializeCriticalSection创建一个临界区,并将其与EnterCriticalSection和LeaveCriticalSection一起使用。

答案 3 :(得分:1)

你的代码充满了问题:

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

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

  • if (lock)测试和lock = 1;代码之间仍然存在竞争条件......即使线程写入原子变量,您也需要比较和交换/比较和交换样式操作,以确保安全,如果失败,你需要旋转等待 - 燃烧CPU - 或经过多次尝试后屈服,然后重试(猜猜是什么) - 到那时你已经重新实现了互斥,而不是使用系统的互联网

答案 4 :(得分:0)

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