C++构造函数内存同步

3

Assume that I have code like:

void InitializeComplexClass(ComplexClass* c);

class Foo {
 public:
  Foo() {
    i = 0;
    InitializeComplexClass(&c);
  }
 private:
  ComplexClass c;
  int i;
};

如果我现在做类似于Foo f;的操作,并将指向f的指针交给另一个线程,那么我有什么保证可以确保InitializeComplexClass()所做的任何存储都对访问f的其他线程的CPU可见?写入i的零的存储呢?我是否需要在类中添加互斥锁,在构造函数中获取写入锁,并在访问成员的任何方法中获取相应的读取锁?
更新:假设一旦构造函数返回,我会将指针交给一堆其他线程。我并不假设代码运行在x86上,而是可能在像PowerPC这样具有进行内存重排序的自由的东西上运行。我感兴趣的实质上是编译器在构造函数返回时必须注入哪些类型的内存屏障。

1
这真的取决于你在线程之间如何共享它。 - NathanOliver
如果Foo被初始化一次,然后被多个线程读取,我建议将const指针传递给其他线程。C++并没有真正的深度或传递性const,但是如果您确保其他线程无法改变成员,您就可以避免锁定。 - Tomek Sowiński
一旦你解决了可见性问题,如果你还没有保护自己,请注意ComplexClass中的非原子更改。 - user4581301
这取决于您如何使用线程,如何共享上下文,以及如何管理内存、共享可变数据和为每个线程设置互斥锁。 - Scott Mudge
@TomekSowiński -- 也许是这样,但这并没有回答在其他线程中对象初始化的可见性问题。正如其他人所说,这取决于对象如何被共享,而const性直到后来才会涉及其中。 - Pete Becker
1
构造函数并不特殊。如果您需要内存屏障,则需要内存屏障。 - Zan Lynx
2个回答

1
为了让另一个线程能够知道你的新对象,你必须交出这个对象/以某种方式通知其他线程。要向线程发出信号,可以写入内存。x86和x64都按顺序执行所有内存写操作,CPU不会对这些操作进行重新排序。这被称为“全存储器顺序”,因此CPU写队列的工作方式类似于“先进先出”。
假设你首先创建一个对象,然后将其传递给另一个线程,那么对内存数据所做的更改也将按顺序发生,并且另一个线程始终以相同的顺序查看它们。在另一个线程了解新对象的时候,这个对象的内容已经保证早些时候对该线程可用(如果该线程只是知道在哪里查找)。
总之,这一次你不需要同步任何东西。在初始化后交出对象就是你需要的所有同步。

更新:在非TSO体系结构中,你没有这个TSO保证。所以你需要同步。使用MemoryBarrier()宏(或任何交错操作),或一些同步API。通过相应的API向另一个线程发出信号也会导致同步,否则它就不会成为同步API。


x86和x64 CPU可能会对写操作进行重新排序,但这与本文无关。只是为了更好的理解-写入可以在读取之后进行排序,因为写入内存会经过写队列,并且刷新该队列可能需要一些时间。另一方面,读缓存始终与其他处理器的最新更新保持一致(这些更新已通过它们自己的写队列)。

这个话题对许多人来说非常困惑,但归根结底,x86-x64程序员只需要担心几件事情:
- 首先,要注意写队列的存在(完全不用担心读缓存!)。
- 其次,在不原子变量长度的情况下,不同线程对同一变量进行并发写入和读取可能会导致数据撕裂,这种情况下您需要同步机制。
- 最后,在多个线程中对同一变量进行并发更新,我们有交错操作,或者再次使用同步机制。


我非常了解TSO和x86,但是这段代码应该是架构无关的,并且只依赖于C++内存模型所保证的内容。 - eof
@eof 一些体系结构会对存储进行重新排序,因此您需要同步。如果您只是将内存写入以发送新对象的指针到其他线程,那是不够的。您应该进行同步。同步实际上由某些交错操作组成,这始终也会导致写队列刷新。 MemoryBarrier() 宏实现了这一点。另一方面,如果您要使用某些同步函数或 API 发出信号,则该 API 已为您执行所有内存同步。 - Roland Pihlakas

0

如果你这样做:

Foo f;
// HERE: InitializeComplexClass() and "i" member init are guaranteed to be completed
passToOtherThread(&f);
/* From this point, you cannot guarantee the state/members
   of 'f' since another thread can modify it */

如果您将实例指针传递给另一个线程,您需要实现保护措施,以便两个线程可以与同一实例交互。如果您仅计划在其他线程上使用该实例,则不需要实现保护措施。但是,请勿像您的示例中那样传递堆栈指针,而应像这样传递新实例:
passToOtherThread(new Foo());

当你完成后,请确保将其删除。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接