C ++手工制作的互斥体

时间:2012-09-11 09:45:29

标签: c++ algorithm mutex atomic mutual-exclusion

我为我的项目制作了一个手工制作的互斥锁,但我怀疑它是否是线程安全的......

    bool blocked;

    while ( blocked )
    {

    }

    blocked = true;
    ...
    blocked = false;

让我们说,线程A传递while循环并且没有及时阻塞标志(没有时间将标志设置为false)而线程B也通过while循环!

  1. 有可能吗?为什么呢?

  2. 当我得到它时,互斥体具有完全相同的工作原理。为什么不能在互斥锁中发生这种情况?我读过关于无法中断的原子操作......所以check-if-mutex-availablemutex-block不能被打断,对吧?

5 个答案:

答案 0 :(得分:8)

您的代码完全不存在了!

原因是对变量blocked的访问不是原子的。如果在第一个线程写出true更新并且更新传播到所有CPU之前发生了两次读取,则两个线程可以同时读取并确定互斥锁已解锁。

你需要原子变量和原子交换来解决这个问题。 atomic_flag类型正是您想要的:

#include <atomic>

std::atomic_flag blocked;

while (blocked.test_and_set()) { }  // spin while "true"

// critical work goes here

blocked.clear();                    // unlock

(或者,您可以使用std::atomic<bool>exchange(true),但atomic_flag专门用于此目的。)

如果这是单线程上下文,原子变量不仅会阻止编译器重新排序出现的代码,而且它们还会使编译器生成必要的代码以防止CPU本身以一种允许不一致的执行流程的方式重新排序指令。

事实上,如果你想提高效率,你可以在集合和清除操作上要求更便宜的内存排序,如下所示:

while (blocked.test_and_set(std::memory_order_acquire)) { }  // lock

// ...

blocked.clear(std::memory_order_release);                    // unlock

原因是你只关心一个方向的正确排序:另一个方向的延迟更新不是很昂贵,但需要顺序一致性(默认情况下)可能很昂贵。


重要提示:以上代码是所谓的自旋锁,因为当状态被锁定时,我们会进行忙碌旋转(while循环)。这在几乎所有情况下都非常糟糕。内核提供的互斥系统调用是一个完全不同的鱼群,因为它允许线程向内核发出信号,它可以进入睡眠状态并让内核取消整个线程的计划。这几乎总是更好的行为。

答案 1 :(得分:3)

  1. 这是可能的,因为处理器可以在进入循环后从第一个线程切换到第二个线程,但是在锁定互斥锁之前。
  2. 这是可能的,因为他们使用内核级操作来确保在某个操作完成之前不切换线程。
  3. 例如,在Windows上,您可以使互斥锁看起来像this

答案 2 :(得分:1)

你已经得到了它。

  1. 是的,这很有可能。对于单核,线程由操作系统通过timeslicing执行。它运行线程A一点,然后暂停它,并运行线程B一点。传递while循环后,线程A可能会暂停。

  2. 为了解决这些问题,CPU已经实现了不能被任何东西打断的特殊指令。这些原子操作由互斥锁用于检查标志,并在一次操作中设置它。

答案 3 :(得分:1)

是的,您所描述的情况可能会发生。这样做的原因是线程可以在测试blockedfalse和将blocked设置为true之间中断。要获得所需的行为,您需要使用或模拟原子测试和设置操作。

有关测试和设置的更多信息,请参见here

答案 4 :(得分:1)

互斥实现必须确保互斥(这是它的含义)而不是在您的代码中获得。它需要一些原子变量和适合其访问的内存顺序。在C ++ 11中,最好使用std::mutex(理想情况下与std::lock一起使用),对于C ++ 03,您可以使用boost::mutex等。