构造函数在C++和/或C++ 11中是否安全?

BCS*_*BCS 22 c++ thread-safety c++11

源自此问题与此问题相关:

如果我在一个线程中构造一个对象,然后将一个引用/指针传递给另一个线程,那么该线程的其他线程在没有显式锁定/内存屏障的情况下访问该对象是不安全的吗?

// thread 1
Obj obj;

anyLeagalTransferDevice.Send(&obj);
while(1); // never let obj go out of scope

// thread 2
anyLeagalTransferDevice.Get()->SomeFn();
Run Code Online (Sandbox Code Playgroud)

或者:是否有任何合法的方法在线程之间传递数据,这些方法不会强制执行与线程触及的其他内容相关的内存排序?从硬件的角度来看,我认为没有任何理由不可能.

澄清; 问题是关于缓存一致性,内存排序等等.在线程2的内存视图包含构造中涉及的写入之前,线程2可以获取并使用指针obj吗?错过引用Alexandrescu(?)"一个恶意的CPU设计师和编译器编写者是否可以共同构建一个标准的符合系统来实现这一目标?"

nos*_*sid 17

关于线程安全的推理可能很困难,而且我不是C++ 11内存模型的专家.但幸运的是,你的例子非常简单.我重写了这个例子,因为构造函数是无关紧要的.

简化示例

问题:以下代码是否正确?或者执行会导致未定义的行为吗?

// Legal transfer of pointer to int without data race.
// The receive function blocks until send is called.
void send(int*);
int* receive();

// --- thread A ---
/* A1 */   int* pointer = receive();
/* A2 */   int answer = *pointer;

// --- thread B ---
           int answer;
/* B1 */   answer = 42;
/* B2 */   send(&answer);
           // wait forever
Run Code Online (Sandbox Code Playgroud)

答案:内存位置可能存在数据争answer,因此执行会导致未定义的行为.请参阅下文了解详情.


实施数据传输

当然,答案取决于函数的可能和法律的实施sendreceive.我使用以下数据无竞争实现.请注意,仅使用单个原子变量,并且所有内存操作都使用std::memory_order_relaxed.基本上这意味着,这些功能不会限制内存重新排序.

std::atomic<int*> transfer{nullptr};

void send(int* pointer) {
    transfer.store(pointer, std::memory_order_relaxed);
}

int* receive() {
    while (transfer.load(std::memory_order_relaxed) == nullptr) { }
    return transfer.load(std::memory_order_relaxed);
}
Run Code Online (Sandbox Code Playgroud)

内存操作顺序

在多核系统上,线程可以按照其他线程看到的不同顺序查看内存更改.此外,编译器和CPU都可以在单个线程内重新排序内存操作以提高效率 - 而且他们一直这样做.原子操作std::memory_order_relaxed不参与任何同步,也不强加任何排序.

在上面的例子中,允许编译器重新排序线程B的操作,并在B1之前执行B2,因为重新排序对线程本身没有影响.

// --- valid execution of operations in thread B ---
           int answer;
/* B2 */   send(&answer);
/* B1 */   answer = 42;
           // wait forever
Run Code Online (Sandbox Code Playgroud)

数据竞赛

C++ 11定义了一个数据竞争如下(N3290 C++ 11草案):"一个程序的执行包含一个数据竞争,如果它包含在不同的线程两个相互矛盾的操作,其中至少一个是不原子,也不发生在另一个之前.任何此类数据竞争都会导致未定义的行为." 之前发生的术语是在同一文档中定义的.

在上面的例子中,B1和A2是冲突和非原子操作,并且都不会发生在另一个之前.这很明显,因为我在上一节中已经表明,两者都可以同时发生.

这是C++ 11中唯一重要的事情.相比之下,如果存在数据竞争,Java内存模型也会尝试定义行为,并且花了将近十年的时间才能得出合理的规范.C++ 11没有犯同样的错误.


更多的信息

我对这些基础知识并不为人所知感到有些惊讶.确切的信息来源是C++ 11标准中的多线程执行和数据争用部分.但是,规范很难理解.

一个很好的起点是Hans Boehm的演讲 - 例如在线视频:

还有很多其他好的资源,我在其他地方提到过,例如: