【问题标题】:C++ constructor thread safetyC++ 构造函数线程安全
【发布时间】:2016-02-24 09:35:29
【问题描述】:

假设我在构造函数中初始化了一个成员变量向量,并且该向量在其他几个成员函数中被读取(不写入其他任何地方)。我是否需要保护对向量的访问(包括在构造函数中),还是保证对象在用于其他线程之前将被完全初始化并刷新到主内存?

让我举个例子:

class A
{
public:
    A();
    void f1();
    void f2();
private:
    std::vector<int> v;
};

A::A()
{
    // do some setup work in v
    v.push_back(1);
}

// called from thread1
void A::f1()
{
    // some readonly work on v
    for (auto i : v) {
        // do something on i
    }
}

// called from thread2
void A::f2()
{
    // more readonly work on v
    if (v.empty()) {
        // do other work
    }
}

我需要在A::A()A::f1()A::f2() 中锁定保护 v 吗?

【问题讨论】:

  • 成员在进入构造函数体之前被初始化,因此肯定在对象本身被构造之前。
  • 最重要的部分是:您如何将对象的引用传递给另一个线程。如果该机制建立了“发生在之前”的关系,那么您是安全的。如果没有,则无论您在构造函数中做什么

标签: c++ multithreading c++11 thread-safety


【解决方案1】:

对象是由单个线程创建的,因此在构造函数中运行涉及成员变量的代码时,您永远不必担心线程安全。但是,如果您在构造函数中使用静态变量,那么您可能需要在访问权限周围添加某种形式的锁定。

存在一种极端情况,即构造函数中的代码可以被多个线程调用,这就是当您使用placement new 时。例如,假设您在某处有一个缓冲区,并且您将在其中分配一个对象:

byte buffer[100];
Foo *foo = new (buffer) Foo;

在这里,除非您锁定对 new 的调用,否则两个或多个构造函数可以并行运行,因为它们针对同一块内存运行。然而,这是一个真正的特殊边缘情况,需要特殊处理(例如锁定新的布局)。

【讨论】:

    【解决方案2】:

    对象由单个线程构造。 其他线程只能通过实例引用访问对象。 换句话说,对象的构造函数将在其他线程调用方法之前完成它的工作。 因此,您不需要在构造函数中实现线程安全代码。

    当然,如果另一个对象作为参数传递给构造函数,则在构造函数中最终访问该对象应该是线程安全的。

    【讨论】:

    • 不确定这是否清楚,但实际上您可以通过传递指针来构建对象,但在这种情况下,您无法在构造函数中做任何事情来防止那场比赛。
    • “应该是线程安全的” -> “需要成为线程安全的”.
    • @MikeMB - 但除非你做一些真正奇怪的事情,否则在构造之前你无法获得指向对象的指针。
    • @Pete:是和否。请参阅 Sean 和我的答案。这可能不常见,但在实际代码中会发生(例如,当双重检查锁定未正确实现或有人忘记时,VS2013 不正确支持 "magic statics")。
    • @MikeMB - 正如我所说,“除非你做一些真正奇怪的事情”; 可能有点太强了……
    【解决方案3】:

    正如其他答案中所述,在构造函数中实现同步原语没有意义,但是如果您不进行外部同步,那并不意味着您不能参加比赛

    std::atomic<A*> g_ptr = nullptr;
    
    void threadFun1() {
        g_ptr.store(new A{}, std::memory_order_relaxed);
    }
    
    void threadFun2() {
        A* l_ptr = nullptr;
        while (l_ptr == nullptr) {
            l_ptr = g_ptr.load(std::memory_order_relaxed);      
        }
        l_ptr->f1();
    }
    

    在上面的代码中,Af1 的构造函数之间存在数据竞争。问题是 - 没有同步 - 从 thread2 的角度来看,g_ptr 可能在对象完全构造之前被写入。

    但是,在构造函数中您无能为力来防止这种竞争。相反,您必须使用外部同步方式,例如对原子加载和存储操作使用非宽松内存排序,或者在设置全局变量后从 thread1 内启动 thread2。

    【讨论】:

      【解决方案4】:

      以下代码示例:

      model.h

      namespace Stackoverflow {
          class Model {
          public:
              Model();
              ~Model();
      
              std::vector<int> *integers() const { return _integers.get(); }; // read only
          private:
              std::unique_ptr<std::vector<int>> _integers; // registered before constructor
          };
      }
      

      model.cpp

      Stackoverflow::Model::Model() {
          _integers = std::make_unique<std::vector<int>>(); // initialized
      }
      
      Stackoverflow::Model::~Model() {
          _integers.release();
      }
      

      私有成员“_integers”将被注册,但在构造函数被调用者调用之前不会被初始化。

      Stackoverflow::Model stackoverflow;
      

      当另一个线程想要访问这个向量时,调用getter。

      auto *vector = stackoverflow.integers();
      

      当调用者实际请求向量时,成员将被完全初始化。

      【讨论】:

      • 所有这些不相关的代码与答案有什么关系?
      • 感谢 @SergeyA 澄清这一点。我只是提供了一些代码,另一个线程如何获取向量。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-09-05
      • 2011-02-10
      • 1970-01-01
      • 2015-03-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多