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

Thread-safe initialization of atomic variable in C++

本文关键字:线程 安全 初始化 变量 C++      更新时间:2023-10-16

请考虑以下 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));
  }
};

你能帮我理解为什么后一个示例有效吗(是因为它不使用相同的互斥锁吗?有没有一种安全的方法可以在B的初始值设定项列表中初始化id,而无需将其锁定在 ctor 的主体中?我的要求是必须const id,并且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( 用作 id . 但是,在线程 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 );
        //  ...
    }
};

(请注意,这需要两次获取互斥锁。 或者,您可以通过完成更新所需的其他信息 sharedResource getNewId(),并让它完成整个工作。

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

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

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

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

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

解决此问题的另一种方法是将其移动到单格尔顿模式中。但是,每次获得对象时,您都需要为锁付费。

现在,您可以进入诸如双重检查锁定:)之类的事情

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