【问题标题】:Implementation of Double Checked Locking in C++ 98/03 using volatile使用 volatile 在 C++ 98/03 中实现双重检查锁定
【发布时间】:2016-09-12 16:14:00
【问题描述】:

阅读 this 关于 C++ 中的双重检查锁定模式的文章,我到达了作者展示使用 volatile 变量“正确”实现 DCLP 的尝试之一的地方(第 10 页):

class Singleton {
public:
  static volatile Singleton* volatile instance();

private:
  static volatile Singleton* volatile pInstance;
};

// from the implementation file
volatile Singleton* volatile Singleton::pInstance = 0;
volatile Singleton* volatile Singleton::instance() {
  if (pInstance == 0) {
    Lock lock;
    if (pInstance == 0) {
      volatile Singleton* volatile temp = new Singleton;
      pInstance = temp;
    }
  }
  return pInstance;
}

在这样的例子之后有一个我不明白的文本sn-p:

首先,标准对可观察行为的限制仅适用于 标准定义的抽象机器,以及该抽象机器 没有多个执行线程的概念。结果,虽然 该标准防止编译器重新排序读取和写入 volatile data within 一个线程,它完全不施加任何约束 这样的重新排序线程。至少大多数编译器是这样的 实施者解释事物。因此,在实践中,许多 编译器可能会从上面的源代码生成线程不安全的代码。

及以后:

... C++ 的抽象机是单线程的,C++ 编译器可以 选择从上面的源代码生成线程不安全的代码, 无论如何。

这些说明与单处理器上的执行有关,因此绝对不是缓存一致性问题。

如果编译器无法对线程中的易失性数据的读取和写入重新排序,那么对于此特定示例,它如何重新排序线程的读取和写入,从而生成线程- 不安全的代码?

【问题讨论】:

  • 那篇文章是 2004 年的,因此比 C++11 及其更新的抽象机器早了很多年。
  • @molbdnilo 好的,但是如何解释编译器可以重新排序线程的读取和写入的语句?

标签: c++ multithreading singleton double-checked-locking


【解决方案1】:

指向单例的指针可能是易变的,但单例的数据不是。

想象一下 Singleton 有 int x, y, z; 作为成员,由于某种原因在构造函数中设置为 15, 16, 17

  volatile Singleton* volatile temp = new Singleton;
  pInstance = temp;

好的,temp 写在pInstance 之前。 x,y,z 是什么时候写的?前?后?你不知道。它们不是 volatile 的,因此不需要相对于 volatile 排序。

现在有一个线程进来看看:

if (pInstance == 0) {  // first check

假设pInstance 已设置,不为空。 x,y,z 的值是多少?即使new Singleton已经被调用,并且构造函数已经“运行”了,你也不知道设置x,y,z的操作是否已经运行了。

所以现在您的代码会读取 x,y,z 并崩溃,因为它真的期待 15,16,17,而不是随机数据。

哦等等,pInstance 是一个指向 volatile 数据的 volatile 指针!所以x,y,z 是不稳定的,对吗?对?因此使用pInstancetemp 订购。啊哈!

几乎。来自*pInstance 的任何读取都将是易失的,但通过new Singleton 的构造不是易失的。所以对x,y,z 的初始写入没有排序。 :-(

所以你可以也许,让成员volatile int x, y, z; OK。不过……

C++现在有一个内存模型,即使在写这篇文章时它还没有。根据当前规则,volatile 不会阻止数据竞争。 volatile 与线程无关。该程序是UB。猫和狗住在一起。

此外,尽管这推动了标准的极限(即,对于 volatile 的真正含义变得模糊),一个无所不知、无所不知、全程序优化的编译器可以查看您对volatile 并说“不,这些 volatile 实际上并没有连接到任何 IO 内存地址等,它们确实不是可观察到的行为,我只是想让它们成为非易失性”...

【讨论】:

  • 对不起,实际上,我在问题中省略了我所指的文章的一部分(我相信这会使问题更清楚)。在该部分中,作者解决了您所描述的问题。构造函数中单例字段的初始化是通过以下方式进行的:static_cast<volatile int&>(x) = 5; 但即使在此之后作者仍然声称这种解决方案不能解决线程不安全代码的生成问题。我知道现代 C++ 有一个详细的内存模型,但作者的意思很有趣。
  • 好的,我现在(重新)阅读了这篇文章(已经有几年了)。我很想说你引用的那段,以及通向下一节的一节的结尾,真的没有多大分量。 (另外,例如,缓存一致性通常不是多处理器的问题(因为它们都(即所有常见架构)确保缓存一致性),问题在于读/写缓冲区。所以这是一个很好的文章,但不一定是完美的。)无论如何,也许大量的 volatile + 单处理器实际上可能是线程安全的。但是volatile 定义不明确,所以我不会指望它。
【解决方案2】:

我认为他们指的是第 6 节(“多处理器机器上的 DCLP”)中讨论的 缓存一致性问题。对于多处理器系统,处理器/缓存硬件可能会写出 @ 的值987654321@ 在为分配的Singleton 写出值之前。这可能导致第二个 CPU 在看到它指向的数据之前看到非 NULL pInstance

这需要一个硬件栅栏指令,以确保在系统中的其他 CPU 可以看到任何内存之前更新所有内存。

【讨论】:

  • 看起来不是这样,因为他们在第 5 节末尾做了一个评论,暗示多处理器执行可能会带来更多问题,除此之外,第 6 节被明确命名为 多处理器机器上的 DCLP 和第 1 节 简介 我们可以阅读:“这篇文章解释了为什么 Singleton 不是线程安全的,DCLP 如何尝试解决这个问题,为什么 DCLP 可能会失败单处理器和多处理器架构,以及为什么您不能(可移植地)对此做任何事情。”
【解决方案3】:

如果我理解正确,他们说的是在单线程抽象机的上下文中,编译器可能会简单地转换:

volatile Singleton* volatile temp = new Singleton;
pInstance = temp;

进入:

pInstance = new Singleton;

因为可观察的行为没有改变。那么这就让我们回到原来的双重检查锁定问题。

【讨论】:

  • 但我认为相反,使用volatile 会使编译器无法进行您提到的优化。根据 C++ 03 标准 1.9.6:“抽象机的可观察行为是其对volatile 数据的读取和写入序列以及对库 I/O 函数的调用”和 1.9.7:“访问由指定的对象一个volatile左值,修改一个对象......都是副作用......在某些指定点......称为序列点,之前评估的所有副作用应该是完整的,并且后续评估的副作用应该没有发生"
猜你喜欢
  • 2011-12-12
  • 1970-01-01
  • 1970-01-01
  • 2020-03-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多