4

考虑以下 C++11 代码,其中类B被实例化并由多个线程使用。因为B修改了共享向量,所以我必须在 ctor 和成员函数 foo 中锁定对它的访问B。为了初始化成员变量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发生在初始化列表中。

4

3 回答 3

2

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

此外,代码中有明确的竞争条件。即使您纠正了上述问题(id用初始化counter ++),假设当前countersharedResource.size()都是0; 你刚刚初始化。线程一进入B, increments的构造函数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() );
    //  ...
}

或者,如果您想制作idconst:

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(),并让它完成整个工作。)

于 2012-04-05T07:50:48.570 回答
1

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

如果有线程安全初始化这样的东西,那就意味着确保一个对象在被初始化之前不能被其他线程访问。

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

于 2012-04-05T01:20:30.547 回答
0

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

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

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

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

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

于 2012-04-05T01:45:23.033 回答