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

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

考虑以下 C++11 代码,其中类 B 被实例化并由多个线程使用。因为 B 修改了一个共享 vector ,所以我必须在 B 的构造函数和成员函数 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));
  }
};

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

最佳答案

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

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

counter == 1
sharedResource.size() == 0

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

在实践中,我会避​​免使用两个单独的变量(countersharedResource.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 );
        //  ...
    }
};

(请注意,这需要获取互斥量两次。或者,您 可以传递完成更新所需的附加信息 sharedResourcegetNewId(),并让它完成整个工作。)

关于c++ - C++中原子变量的线程安全初始化,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10017357/

相关文章:

c++ - 为什么 throw() 和 noexcept 没有任何开销?

c++ - std::thread调用类方法

java - 从 ConcurrentMap.remove() 获取键是否存在

c++ - 如何展开模板特化

c++ - -std=c++0x 和 -std=c++11 有什么区别

c++ - 计算 2 的幂的意外结果 [C++]

c#-4.0 - Task.ContinueWith 线程安全吗?

initializer_lists 平均值的 C++11 通用函数

c++ - 为什么我必须为 QLabel 动态分配内存才能工作?