C ++中原子变量的线程安全初始化

时间:2012-04-04 18:44:11

标签: c++ thread-safety c++11 mutex atomic

考虑以下C ++ 11代码,其中类B被实例化并由多个线程使用。因为B修改了共享向量,所以我必须在B的ctor和成员函数foo中锁定对它的访问。要初始化成员变量id,我使用一个原子变量计数器,因为我从多个线程访问它。

struct A {
  A(size_t id, std::string const& sig) : id{id}, signature{sig} {}
private:
  size_t id;
  std::string signature;
};
namespace N {
  std::atomic<size_t> counter{0};
  typedef std::vector<A> As;
  std::vector<As> sharedResource;
  std::mutex barrier;

  struct B {
    B() : id(++counter) {
      std::lock_guard<std::mutex> lock(barrier);
      sharedResource.push_back(As{});
      sharedResource[id].push_back(A("B()", id));
    }
    void foo() {
      std::lock_guard<std::mutex> lock(barrier);
      sharedResource[id].push_back(A("foo()", id));
    }
  private:
    const size_t id;
  };
}

不幸的是,此代码包含竞争条件,并且不能像这样工作(有时ctor和foo()不使用相同的id)。如果我将id的初始化移动到由互斥锁锁定的ctor主体,它可以工作:

struct B {
  B() {
    std::lock_guard<std::mutex> lock(barrier);
    id = ++counter; // counter does not have to be an atomic variable and id cannot be const anymore
    sharedResource.push_back(As{});
    sharedResource[id].push_back(A("B()", id));
  }
};

你能帮我理解为什么后一个例子有效(是因为它不使用相同的互斥体?)?有没有一种安全的方法可以在id的初始化列表中初始化B而不将其锁定在ctor的主体中?我的要求是id必须为const,并且id的初始化发生在初始化列表中。

3 个答案:

答案 0 :(得分:2)

首先,发布的代码中仍然存在一个基本的逻辑问题。 您使用++ counter作为id。考虑B的第一次创建, 在一个线程中。 Bid == 1;在push_back之后 sharedResource,您将拥有sharedResource.size() == 1,以及0 只有访问它的合法索引才是id

此外,代码中存在明显的竞争条件。即使你 请更正上述问题(使用counter ++初始化countersharedResource.size()0目前都是B; 你刚刚初始化了。线程1进入counter的构造函数, 增加counter == 1 sharedResource.size() == 0 ,所以:

counter

然后它被线程2中断(在它获取互斥锁之前) 也会增加id(到2),并使用其先前的值(1)作为 push_back。然而,在线程2中的sharedResource.size() == 1之后,我们只有 counter,唯一的合法索引是0。

在实践中,我会避​​免两个单独的变量(sharedResource.size()assert( id == sharedResource.size() ))应具有相同的值。从 经验:应该是相同的两件事不会是唯一的 时间冗余信息应该用于何时使用 控制;即在某些时候,你有B::B() { std::lock_guard<std::mutex> lock( barrier ); id = sharedResource.size(); sharedResource.push_back( As() ); // ... } 或类似的东西。我会使用类似的东西:

id

或者如果你想让struct B { static int getNewId() { std::lock_guard<std::mutex> lock( barrier ); int results = sharedResource.size(); sharedResource.push_back( As() ); return results; } B::B() : id( getNewId() ) { std::lock_guard<std::mutex> lock( barrier ); // ... } }; const:

sharedResource

(请注意,这需要两次获取互斥锁。或者,您 可以传递完成更新所需的其他信息 getNewId()到{{1}},让它完成整个工作。)

答案 1 :(得分:1)

当一个对象被初始化时,它应该由一个线程拥有。然后,当它被初始化时,它被共享。

如果存在线程安全初始化这样的事情,则意味着确保在初始化之前其他线程无法访问该对象。

当然,我们可以讨论原子变量的线程安全assignment。分配与初始化不同。

答案 2 :(得分:0)

您在初始化向量的子构造函数列表中。这不是真正的原子操作。因此在多线程系统中,您可能会同时受到两个线程的攻击。这正在改变id是什么。欢迎来到安全线101!

将初始化移动到由锁定所包围的构造函数中,这样只有一个线程可以访问并设置向量。

解决这个问题的另一种方法是将其转变为singelton模式。但是每当你拿到物体时,你就会为锁付钱。

现在你可以进入像双重检查锁定的东西:)

http://en.wikipedia.org/wiki/Double-checked_locking