简体   繁体   English

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

[英]Thread-safe initialization of atomic variable in C++

Consider the following C++11 code where class B is instantiated and used by multiple threads.考虑以下 C++11 代码,其中 class B被实例化并由多个线程使用。 Because B modifies a shared vector, I have to lock access to it in the ctor and member function foo of B .因为B修改了共享向量,所以我必须在B的构造函数和成员 function foo 中锁定对它的访问。 To initialize the member variable id I use a counter that is an atomic variable because I access it from multiple threads.为了初始化成员变量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;
  };
}

Unfortunately, this code contains a race condition and does not work like this (sometimes the ctor and foo() do not use the same id).不幸的是,这段代码包含竞争条件并且不能像这样工作(有时 ctor 和 foo() 不使用相同的 id)。 If I move the initialization of id to the ctor body which is locked by a mutex, it works:如果我将 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));
  }
};

Can you please help me understanding why the latter example works (is it because it does not use the same mutex?)?你能帮我理解为什么后一个例子有效吗(是因为它没有使用相同的互斥量吗?)? Is there a safe way to initialize id in the initializer list of B without locking it in the body of the ctor?有没有一种安全的方法可以在B的初始化列表中初始化id而无需将其锁定在 ctor 的主体中? My requirements are that id must be const and that the initialization of id takes place in the initializer list.我的要求是id必须是const并且id的初始化发生在初始化列表中。

First, there's still a fundamental logic problem in the posted code.首先,发布的代码中仍然存在一个基本的逻辑问题。 You use ++ counter as id .您使用++ counter作为id Consider the very first creation of B , in a single thread.考虑在单个线程中首次创建B B will have id == 1 ; B将有id == 1 after the push_back of sharedResource , you will have sharedResource.size() == 1 , and the only legal index for accessing it will be 0 .sharedResourcepush_back之后,您将拥有sharedResource.size() == 1 ,并且访问它的唯一合法索引将是0

In addition, there's a clear race condition in the code.此外,代码中存在明显的竞争条件。 Even if you correct the above problem (initializing id with counter ++ ), suppose that both counter and sharedResource.size() are currently 0 ;即使您更正了上述问题(使用counter ++初始化id ),假设countersharedResource.size()当前均为0 you've just initialized.你刚刚初始化。 Thread one enters the constructor of B , increments counter , so:线程一进入B的构造函数,递增counter ,因此:

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

It is then interrupted by thread 2 (before it acquires the mutex), which also increments counter (to 2), and uses its previous value (1) as id .然后它被线程 2 中断(在它获取互斥锁之前),线程 2 也将counter递增(到 2),并将其先前的值 (1) 用作id After the push_back in thread 2, however, we have only sharedResource.size() == 1 , and the only legal index is 0.然而,在线程 2 中的push_back之后,我们只有sharedResource.size() == 1 ,唯一合法的索引是 0。

In practice, I would avoid two separate variables ( counter and sharedResource.size() ) which should have the same value.实际上,我会避免两个单独的变量( countersharedResource.size() ),它们应该具有相同的值。 From experience: two things that should be the same won't be—the only time redundant information should be used is when it is used for control;根据经验:应该相同的两件事不会相同——唯一应该使用冗余信息的时间是在将其用于控制时; ie at some point, you have an assert( id == sharedResource.size() ) , or something similar.即在某些时候,您有一个assert( id == sharedResource.size() )或类似的东西。 I'd use something like:我会使用类似的东西:

B::B()
{
    std::lock_guard<std::mutex> lock( barrier );
    id = sharedResource.size();
    sharedResource.push_back( As() );
    //  ...
}

Or if you want to make id const:或者如果你想使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 );
        //  ...
    }
};

(Note that this requires acquiring the mutex twice. Alternatively, you could pass the additional information necessary to complete updating sharedResource to getNewId() , and have it do the whole job.) (请注意,这需要获取互斥量两次。或者,您可以将完成更新sharedResource所需的附加信息传递给getNewId() ,并让它完成整个工作。)

When an object is being initialized, it should be owned by a single thread.初始化 object 时,它应该由单个线程拥有。 Then when it is done being initialized, it is made shared.然后当它完成初始化时,它就被共享了。

If there is such a thing as thread-safe initialization, it means ensuring that an object has not become accessible to other threads before being initialized.如果存在线程安全初始化这样的事情,则意味着确保 object 在初始化之前没有变得可供其他线程访问。

Of course, we can discuss thread-safe assignment of an atomic variable.当然,我们可以讨论原子变量的线程安全assignment Assignment is different from initialization.赋值不同于初始化。

You are in the sub-constructor list initializing the vector.您在初始化向量的子构造函数列表中。 This is not really an atomic operation.这不是真正的原子操作。 so in a multi-threaded system you could get hit by two threads at the same time.所以在多线程系统中,你可能会同时被两个线程击中。 This is changing what id is.这正在改变 id 是什么。 welcome to thread safety 101!欢迎来到线程安全 101!

moving the initialization into the constructor surrounded by the lock makes it so only one thread can access and set the vector.将初始化移动到被锁包围的构造函数中,这样只有一个线程可以访问和设置向量。

The other way to fix this would be to move this into a singelton pattern.解决此问题的另一种方法是将其移动到单例模式中。 But then you are paying for the lock every time you get the object.但是,每次获得 object 时,您都需要为锁付费。

Now you can get into things like double checked locking:)现在你可以进入双重检查锁定之类的东西了:)

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM