【问题标题】:Assignment via copy-and-swap vs two locks通过复制和交换与两个锁进行分配
【发布时间】:2011-07-01 14:53:46
【问题描述】:

借用 Howard Hinnant's example 并修改它以使用复制和交换,这 op= 线程安全吗?

struct A {
  A() = default;
  A(A const &x);  // Assume implements correct locking and copying.

  A& operator=(A x) {
    std::lock_guard<std::mutex> lock_data (_mut);
    using std::swap;
    swap(_data, x._data);
    return *this;
  }

private:
  mutable std::mutex _mut;
  std::vector<double> _data;
};

我相信这是线程安全的(记住 op= 的参数是按值传递的),我能找到的唯一问题是隐藏在地毯下的问题:复制 ctor。但是,这将是一个罕见的类,它允许复制分配但不允许复制构造,因此在两种替代方案中同样存在这个问题。

鉴于自我分配是如此罕见(至少对于本示例而言),如果发生这种情况,我不介意额外的副本,因此请考虑 this != &rhs 的潜在优化可以忽略不计或悲观化。与原来的策略(下)相比,是否还有其他理由更喜欢或避免它?

A& operator=(A const &rhs) {
  if (this != &rhs) {
    std::unique_lock<std::mutex> lhs_lock(    _mut, std::defer_lock);
    std::unique_lock<std::mutex> rhs_lock(rhs._mut, std::defer_lock);
    std::lock(lhs_lock, rhs_lock);
    _data = rhs._data;
  }
  return *this;
}

顺便说一句,我认为这简洁地处理了复制ctor,至少对于这个类来说,即使它有点迟钝:

A(A const &x) : _data {(std::lock_guard<std::mutex>(x._mut), x._data)} {}

【问题讨论】:

  • An answer 提示我提出这个问题。
  • 作为链接答案的原始作者,并且做出了相同的决定(使用按值传递以避免双重锁定),我觉得我必须说明两种方法之间的关键区别,即碰巧评论太长了,所以我正在编辑答案。

标签: c++ c++11 mutex copy-and-swap


【解决方案1】:

我相信你的分配是线程安全的(当然假设在课堂之外没有引用)。它相对于const A&amp; 变体的性能可能取决于A。我认为对于许多A 来说,你的重写速度即使不是更快也一样快。我有一个很大的反例是 std::vector (和类似的类)。

std::vector 具有不参与其值的容量。如果 lhs 相对于 rhs 有足够的容量,那么重用这个容量,而不是把它扔到一个临时的,可以是性能上的胜利。

例如:

std::vector<int> v1(5);
std::vector<int> v2(4);
...
v1 = v2;

在上面的例子中,如果 v1 保持其执行分配的能力,那么分配可以在没有堆分配或释放的情况下完成。但是如果vector使用了swap idiom,那么它会进行一次分配和一次释放。

我注意到,就线程安全而言,两种算法都会锁定/解锁两个锁。尽管交换变体避免了同时锁定它们的需要。我相信平均而言同时锁定两者的成本很小。但在竞争激烈的用例中,它可能会成为一个问题。

【讨论】:

  • 如果复制赋值运算符的参数是右值,则仍然可以省略复制。而且它仍然是线程安全的! :-) 当你引用了一个右值时,你可以确信没有其他人,甚至另一个线程,都引用了相同的右值。因此,对那个右值做任何你想做的事情都是安全的。我所说的一个例外是,如果有人将左值转换为右值。但在这种情况下,即使在多线程环境中,确保程序能够真正将转换后的左值视为右值也是施法者的责任。
  • @7vies:你的结论部分正确。由每个类的作者来确定复制分配的最佳算法。有时这将是复制构建和交换,有时则不会。类似向量的成员的存在可能会影响这个决定,也可能不会。它将取决于所有成员/基础,并且还可能取决于复制分配运算符的最可能用例。只是没有绕过性能测试。 :-)
  • 我明白了。我记得复制和交换有时就像银弹一样,现在我知道它实际上取决于它。再次感谢!
  • @7vies:虽然性能可能很重要,但我通常将分配实现为复制和交换,因为它更简单。由于这是一个实现细节,如有必要,我随时可以回来修改它,但同时至少我的代码是正确的。
  • @DavidRodríguez-dribeas:复制和交换是否也定义了“何时发生一致性”?即,分配“开始”时的值(因为我们没有将分配视为原子)。在这两种情况下,您都不能确保,例如:a = b; assert(a == b);
猜你喜欢
  • 2013-11-19
  • 1970-01-01
  • 1970-01-01
  • 2013-10-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多