6

借用Howard Hinnant 的示例并将其修改为使用复制和交换,这 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。但是,这将是一个罕见的类,它允许复制分配但不允许复制构造,因此该问题同样存在于两种替代方案中。

鉴于自我分配是如此罕见(至少对于这个例子来说),如果它发生我不介意额外的副本,考虑这个 != &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)} {}
4

1 回答 1

8

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

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

例如:

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

在上面的示例中,如果 v1 保持其执行分配的能力,则可以在没有堆分配或释放的情况下完成分配。但是如果向量使用交换习语,那么它会进行一次分配和一次释放。

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

于 2011-02-21T23:15:56.587 回答