关于 c++ 初始化程序的内存一致性

Posted

技术标签:

【中文标题】关于 c++ 初始化程序的内存一致性【英文标题】:Memory coherence with respect to c++ initializers 【发布时间】:2015-09-18 13:23:00 【问题描述】:

如果我在一个线程中设置变量的值并在另一个线程中读取它,我会用锁保护它以确保第二个线程读取第一个线程最近设置的值:

线程 1:

lock();
x=3;
unlock();

线程 2:

lock();
<use the value of x>
unlock();

到目前为止,一切都很好。但是,假设我有一个 c++ 对象,它在初始化程序中设置 x 的值:

theClass::theClass() : x(3) ...
theClass theInstance;

然后,我生成一个使用实例的线程。是否可以保证新生成的线程会看到正确的 x 值?或者是否有必要在 theInstance 的声明周围加锁?我主要对 Linux 上的 c++ 感兴趣。

【问题讨论】:

特定于编程和软件开发的问题不在主题范围内,请参阅What topics can I ask about here?。 【参考方案1】:

在 C++11 之前,C++ 标准对多线程执行没有任何规定,因此不保证任何事情。

C++11 引入了一种内存模型,该模型定义了在什么情况下写入一个线程的内存保证对另一个线程可见。

对象的构造本质上不是跨线程同步的。但是,在您的特定情况下,您说您首先构造对象,然后“生成线程”。如果您通过构造std::thread 对象来“生成线程”,并且在同一线程上构造了一些对象x 之后执行此操作,那么您可以保证在新生成的线程上看到x 的正确值。这是因为thread构造函数的完成synchronizes-with是你线程函数的开始。

synchronizes-with 一词是用于定义 C++ 内存模型的特定术语,值得准确理解 what it means 以了解更复杂的同步,但在您概述的情况下“正常工作”无需任何额外的同步。

这一切都假设您使用的是std::thread。如果您直接使用平台线程 API,则 C++ 标准对发生的情况无话可说,但实际上您可以假设它无需锁定我所知道的任何平台即可工作。

【讨论】:

你混淆了不必要的 OP。他无法在对象构建之前访问它,因此“内存模型”(您使用不正确的术语,正确的是内存排序,模型来自 1994 年左右)是无关紧要的。 @SergeyA 我是using term memory model 正确。【参考方案2】:

你似乎对锁有误解:

如果我在一个线程中设置变量的值并在另一个线程中读取它, 我用锁保护它,以确保第二个线程读取 第一个最近设置的值。

这是不正确的。锁用于防止数据竞争。锁不会将线程 1 的指令安排在线程 2 的指令之前发生。有了锁,线程 2 仍然可以在线程 1 之前运行,并在线程 1 之前读取 x 的值更改x 的值。

至于你的问题:

    如果您对theInstance 的初始化发生在某个线程A 的初始化/启动之前,则可以保证线程A 看到x 的正确值。

示例

#include <thread>
#include <assert.h>
struct C

    C(int x) : x_ x  
    int x_;
;

void f(C const& c)

    assert(c.x_ == 42);


int main()

    C c 42 ;                       // A
    std::thread t f, std::ref(c) ; // B
    t.join();

在同一个线程中:A 是 sequenced-before B,因此 A happens-before B。线程 t 中的断言因此永远不会触发。 p>

    如果您对“theInstance”的初始化线程间发生之前它被某个线程 A 使用,那么线程 A 可以保证看到 x 的正确值。

示例

#include <thread>
#include <atomic>
#include <assert.h>

struct C

    int x_;
;

std::atomic<bool> is_init;

void f0(C& c)

    c.x_ = 37;               // B
    is_init.store(true);     // C


void f1(C const& c)

    while (!is_init.load()); // D

    assert(c.x_ == 37);      // E


int main()

    is_init.store(false); // A
    C c;
    std::thread t0 f0, std::ref(c) ;
    std::thread t1 f1, std::ref(c) ;
    t0.join();
    t1.join();

t0t1 之间发生线程间的happens-before 关系。和以前一样,A 发生在创建线程 t0t1 之前。

分配c.x_ = 37 (B) 发生在存储到is_init 标志(C) 之前。 f1 中的循环是线程间发生之前关系的来源:f1 仅在设置了 is_init 后继续执行,因此 C 发生在 E 之前.由于这些关系是可传递的,B 线程间发生在 D 之前。因此,断言永远不会在f1 中触发。

【讨论】:

【参考方案3】:

首先,您上面的示例不保证任何锁定。您需要做的就是声明您的变量是原子的。没有锁,不用担心。

其次,你的问题并没有什么意义。由于在构造对象(类的实例)之前不能使用它,并且构造是在单线程中进行的,因此无需锁定在类构造函数中完成的任何操作。您根本无法从多个线程访问非构造类,这是不可能的。

【讨论】:

如果没有正确同步,可以从另一个线程观察到部分构造的对象。 我看不出这怎么可能。对象必须先构造后才能使用,然后才能在另一个对象中使用。显然,如果您正在谈论将此对象的某种“发送”地址到另一个线程,并且这种“发送”本身不是原子的,那么对象状态将处于混乱状态。但这意味着消息传递已损坏。使用声音消息是不可能的。 但这正是问题所要问的(至少据我了解) - 通过在另一个线程上创建的“已发布”地址访问对象是否安全,而无需显式同步。一般来说,答案是否定的,但在这种情况下,线程本身的创建足以同步以确保“发布”是可见的,即使它只是一个常规(非原子)指针分配。 我没有这样读,而且问题(我是否需要锁定实例声明)并没有向我表明这一点。在任何情况下,实例创建都不需要锁定。始终需要适当的线程通信机制。

以上是关于关于 c++ 初始化程序的内存一致性的主要内容,如果未能解决你的问题,请参考以下文章

C++总结体会

引发C++程序内存错误的常见原因分析与总结

引发C++程序内存错误的常见原因分析与总结

引发C++软件异常的常见原因分析

小白学习C++ 教程十六C++ 中的动态内存分配

C++内存管理机制