3

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

    bool blocked;

    while ( blocked )
    {

    }

    blocked = true;
    ...
    blocked = false;

可以说,线程 A 通过了 while 循环并且没有及时阻塞标志(没有时间将标志设置为false),线程 B 也通过了 while 循环!

  1. 可能吗?为什么?

  2. 据我了解,互斥锁具有完全相同的工作原理。为什么这不能发生在互斥体中?我读过关于不能被中断的原子操作......所以check-if-mutex-availablemutex-block不能被中断,对吧?

4

5 回答 5

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>and exchange(true),但 theatomic_flag是专门为此目的而制作的。)

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

事实上,如果你想稍微提高一点效率,你可以在 set 和 clear 操作上要求更便宜的内存排序,如下所示:

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

// ...

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

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


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

于 2012-09-11T09:51:23.890 回答
3
  1. 这是可能的,因为处理器可以在进入循环后但在锁定互斥体之前从第一个线程切换到第二个线程。
  2. 这是可能的,因为它们使用内核级操作来确保在某个操作完成之前不会切换线程。

例如,在 Windows 上,您可以使互斥锁看起来像这样

于 2012-09-11T09:53:14.263 回答
1

你已经得到了它。

  1. 是的,很有可能。对于单核,操作系统通过时间片执行线程。它运行线程 A 一段时间,然后暂停它,然后运行线程 B 一段时间。通过 while 循环后,线程 A 可能会立即暂停。

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

于 2012-09-11T09:51:44.400 回答
1

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

可以在此处找到有关测试和设置的更多信息。

于 2012-09-11T09:52:28.490 回答
1

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

于 2012-09-11T09:54:35.120 回答