2

对双重检查锁定的修复有什么问题的评论?说:

问题是变量可能在构造函数运行(或完成)之前分配,而不是在分配对象之前。

让我们考虑代码:

A *a;

void Test()
{
    a = new A;
}

为了进行更正式的分析,让我们将 a = new A 拆分为几个操作:

void *mem = malloc(sizeof(A)); // Allocation
new(mem) A; // Constructor
a = reinterpret_cast<A *>(mem); // Assignment

上面引用的评论是否属实,如果属实,在什么意义上?构造函数可以在赋值之后执行吗?如果可以,当因为 MT 安全而需要保证订单时,可以采取什么措施?

4

4 回答 4

1

a是一个具有静态存储持续时间的全局对象,因此它将在 main 主体执行之前的某个时间在一些预分配的存储中初始化。假设对 Test 的调用不是某些静态对象构造怪异的结果,a将在调用 Test 时完全构造。

a = new A;

这种稍微不寻常的分配不会(仅)是标准的复制分配操作,因为您将指针分配A给 a,而不是对象或引用。它是否真正编译以及它到底调用了什么取决于是否A有一个赋值运算符,它接受一个指向的指针A,或者从一个指针隐式转换的东西,A或者是否A有一个非显式的构造函数,它接受一个指向的指针A(或一个指向基的指针类A)。

编辑后,您的代码做了一些不同的事情!

从概念上讲,它更像这样:

A *tmpa;
void *mem = ::operator new( sizeof(A) ); // ( or possibly A::operator new )

try
{
    tmpa = new (mem) A; // placement new = default constructor call
}
catch (...)
{
    ::operator delete( mem );
    throw;
}

a = tmpa; // pointer assignment won't throw.

写出这样的东西的危险在于,您隐式添加了许多原始序列中不存在的序列点,此外,编译器允许生成看起来不像这样的代码,只要它的行为就执行程序可以确定的情况而言,“好像”它是由 this 编写的。这个“好像”规则仅适用于正在执行的线程,因为(当前)语言没有说明与其他线程的交互是否有效。

为此,您需要使用您的实现提供的特定行为保证(如果有)。

于 2009-06-17T21:55:45.547 回答
1

问题不在于代码执行时,而更多地与写顺序有关。

让我们假设:

A()
{
   member = 7;
}

然后后来:

singleton = new A()

这导致代码执行分配,写入内存(成员),然后写入另一个内存位置(单例)。一些 CPU 可以重新排序写入,以便在写入单例之后才能看到对成员的写入 - 本质上,在系统中其他 CPU 上运行的代码可以查看写入单例的内存,但成员是不是。

于 2009-06-17T22:06:28.350 回答
1

我认为以下应该工作:

void Test()
{
    A *temp = new A;
    MemoryWriteBarrier(); // use whatever memory barrier your platform offers
    a = temp;
}
于 2009-06-17T22:07:06.163 回答
-1

是的,可以在赋值后调用构造函数,尽管您给出的示例在内部不一致(如评论所述)。

您可以添加一些锁以让您安心,但也很容易弄错。

“C++ 和双重检查锁定的危险”

斯科特迈耶斯和安德烈亚历山德雷斯库

http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf

于 2009-06-17T22:04:50.053 回答