【问题标题】:C++ hand-made mutexC++ 手工制作的互斥锁
【发布时间】:2012-09-04 05:18:14
【问题描述】:

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

    bool blocked;

    while ( blocked )
    {

    }

    blocked = true;
    ...
    blocked = false;

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

  1. 有可能吗?为什么?

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

【问题讨论】:

  • 我的第一个建议是不要这样做。我的第二个建议是不要这样做!使用像 std::mutex 这样的正确互斥体,它们经过测试,可以工作并且绝对是线程安全的。
  • 我想使用pthread_mutex_t。这个实现好吗?我既不能使用boost 也不能使用c++11
  • pthread 互斥锁很好。
  • @JoachimPileborg 谢谢!

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


【解决方案1】:
  1. 这是可能的,因为处理器可以在进入循环后但在锁定互斥体之前从第一个线程切换到第二个线程。
  2. 这是可能的,因为它们使用内核级操作来确保在某个操作完成之前不会切换线程。

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

【讨论】:

  • 您的代码需要非标准功能......甚至不包括那些原子访问功能
  • 这就是“例如在 Windows 上”的原因。我不知道今天的编译器对 C++11 线程工具的支持程度,所以这是一个合乎逻辑的选择。如果必须切换到 Linux,那看起来并没有太大的不同。
  • 我想使用pthread_mutex_t。这个实现好吗?我既不能使用boost 也不能使用c++11
  • 是的,它相当于我在类 unix 系统上发布的内容。
【解决方案2】:

你的代码完全失效了!

原因是对变量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&lt;bool&gt;exchange(true),但atomic_flag 是专门为此目的而制作的。)

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

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

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

// ...

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

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


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

【讨论】:

  • 这个解决方案仍然不是异常安全的。如果“关键工作”会抛出异常怎么办?只需使用 std::mutex 或 boost::mutex,它们的实现就足够快了
  • 嗯,不应该是while (blocked.test_and_set()); do_critical_work(); blocked.clear();吗?
  • @nogard:不,这不是异常安全的。互斥体也不是。
  • @KerrekSB mutex::lock 是异常安全的,因为当抛出异常时 -> 堆栈展开 -> 锁被破坏 -> 互斥锁被解锁。这是锁卫士的主要用途。
  • @nogard:您可以轻松地在他的代码周围创建一个像包装器一样的锁守卫,问题出在哪里?他只是准确地描述了一个自制的想要互斥体,而不是更多。就像您将他的描述替换为 boost mutex 的内部实现细节一样,也没有什么不同(例外安全方面)。
【解决方案3】:

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

【讨论】:

  • 我想使用pthread_mutex_t。这个实现好吗?我既不能使用boost 也不能使用c++11
【解决方案4】:

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

关于测试和设置的更多信息可以找到here

【讨论】:

  • 更严重的原因是不能保证线程A和B看到变量blocked的值相同:它不是原子变量,访问它的线程之间不存在同步条件
【解决方案5】:

你已经差不多了。

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-07-31
    • 2018-03-27
    • 1970-01-01
    • 2018-05-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多