线程安全的复制构造函数/赋值运算符

时间:2010-10-27 21:25:38

标签: c++ c++11 thread-safety

假设我们想使用A使类std::mutex成为线程安全的。我的复制构造函数和赋值运算符与下面的代码类似:

#include <mutex>

class A {
private:
  int i;
  mutable std::mutex mtx;

public:
  A() : i(), mtx() { }

  A(const A& other) : i(), mtx()
  {
    std::lock_guard<std::mutex> _lock(other.mtx);
    i = other.i;
  }

  A& operator=(const A& other)
  {
    if (this!=&other) {
      std::lock_guard<std::mutex> _mylock(mtx), _otherlock(other.mtx);
      i = other.i;
    }
    return *this;
  }

  int get() const
  {
    std::lock_guard<std::mutex> _mylock(mtx);
    return i;
  }
};

我不认为它有任何问题,除了other被复制之前被另一个线程销毁的可能性,我可以处理。

我的问题是我没有在任何地方看到这种模式,所以我不知道人们是否只是没有需要,或者由于我目前看不到的原因显然是错误的。

由于

注意

这只是一个例子。我可以拥有任意类型的任意数量的成员变量,它不必只是int

在Martin York对可能的死锁的评论之后,这是一个使用复制和交换的更新版本(复制省略也是可能的,但它无法有效处理自我分配案例)。

我也将int改为T,所以人们不能认为它是POD。

template<typename T>
class A {
private:
  T t;
  mutable std::mutex mtx;

public:
  A() : t(), mtx() { }

  A(const A& other) : t(), mtx()
  {
    std::lock_guard<std::mutex> _lock(other.mtx);
    t = other.t;
  }

  A& operator=(const A& other)
  {
    if (this!=&other) {
      A tmp(other);
      std::lock_guard<std::mutex> _lock(mtx);
      std::swap(t, tmp.t);
    }
    return *this;
  }

  T get() const
  {
    std::lock_guard<std::mutex> _lock(mtx);
    return t;
  }

};

5 个答案:

答案 0 :(得分:4)

老问题,新答案:

Imho,处理原始复制赋值运算符的死锁问题的更好方法是:

  A& operator=(const A& other)
  {
    if (this!=&other) {
      std::unique_lock<std::mutex> _mylock(mtx, std::defer_lock),
                                   _otherlock(other.mtx, std::defer_lock);
      std::lock(_mylock, _otherlock);
      i = other.i;
    }
    return *this;
  }

即。使用std::lock(L1, L2)同时锁定两个互斥锁而不用担心死锁。这可能比复制/交换习惯用语具有更高的性能,特别是如果成员数据由std::vectorstd::string或包含向量和/或字符串的类型组成。

在C ++ 1y中(我们希望y为4),有一个新的<shared_mutex>标头提供读/写锁定功能,可能提供性能提升(性能测试将是具体用例需要确认)。以下是它的使用方法:

#include <mutex>
#include <shared_mutex>

class A {
private:
  int i;
  mutable std::shared_mutex mtx;

public:
  A() : i(), mtx() { }

  A(const A& other) : i(), mtx()
  {
    std::shared_lock<std::shared_mutex> _lock(other.mtx);
    i = other.i;
  }

  A& operator=(const A& other)
  {
    if (this!=&other) {
      std::unique_lock<std::shared_mutex> _mylock(mtx, std::defer_lock);
      std::shared_lock<std::shared_mutex> _otherlock(other.mtx, std::defer_lock);
      std::lock(_mylock, _otherlock);
      i = other.i;
    }
    return *this;
  }

  int get() const
  {
    std::shared_lock<std::shared_mutex> _mylock(mtx);
    return i;
  }
};

即。这与原始代码非常相似(修改为使用std::lock,就像我上面所做的那样)。但是成员互斥锁类型现在是std::shared_mutex而不是std::mutex。并且当保护const A(并且假设除了互斥锁之外没有可变成员)时,只需要在“共享模式”下锁定互斥锁。使用shared_lock<shared_mutex>可以轻松完成此操作。当您需要以“独占模式”锁定互斥锁时,您可以根据需要使用unique_lock<shared_mutex>lock_guard<shared_mutex>,并使用与std::mutex使用此功能相同的方式。

特别注意,现在许多线程可以同时从同一A复制构造,甚至可以从复制同一A。但是,一次只能有一个帖子可以复制分配同一个A

答案 1 :(得分:2)

忽略所有实现细节,您没有看到此模式的原因是因为很可能您正在锁定错误的抽象级别。

  • 如果从多个线程访问对象,则您(另外)必须管理对象的生命周期,这些对象无法在对象内进行管理。
  • 对于终身管理,您至少需要一个对象外部锁,因此最好使用它。
  • 此方案仅适用于单一get()对象 - 如果您的对象具有比一个get()函数更多(超过一个成员和更多成员),则从该对象读取可能/将导致数据不一致。

获取正确的多线程代码不仅仅是确保没有“崩溃”并且单个对象保持一致状态。如果你(认为你)需要上述方案,你可能会认为当你的应用仍在做错事时你会保存。

至于实现细节:由于您已经在使用C ++ 0x,因此您还应该实现适当定义的移动操作。

答案 2 :(得分:1)

我不是这方面的权威,因为多线程很棘手,但到目前为止看起来很好 。 顺便说一下,你可能意味着

std::lock_guard<std::mutex>

并在copy-ctor中:

A(const A& other) : mtx()
{
  std::lock_guard<std::mutex> _lock(other.mtx);
  i = other.i;
}

确保other的线程安全的另一种方法是仅使用“安全”的getter来访问它,尽管在调用多个getter时这不会按预期运行。但是,要小心参考!

答案 3 :(得分:0)

你没有真正看到它,因为标准的线程设施非常新,而且我不知道支持它们的单个编译器 - 你会进一步寻找boost :: thread示例。此外,您无偿使用同步可能会导致性能不佳,但这只是我的意见。

答案 4 :(得分:0)

这更正确,但并不完全健壮:

#include <mutex>

class A {
private:
    int i;
    std::mutex mtx;

public:
    A() : i(0), mtx() {
    }
    /* this is one option for implementation, but would be rewritten when there are more ivars in order to reduce acquisition counts */
    A(A& other) : i(other.get()), mtx() {
    }

    ~A() {
        /* unsafe if subclassed, also useful for determining whether objects are destroyed prematurely (e.g., by their containers */
        std::lock_guard<std::mutex> _mylock(this->mtx);
    }

    A& operator=(A& other) {
        std::lock_guard<std::mutex> _mylock(this->mtx);
        std::lock_guard<std::mutex> _otherlock(other.mtx);
        this->i = other.i; /* you could call other.get() and bypass the lock_guard, but i'm assuming there's really more work to be done here */
        return *this;
    }

    int get() {
        std::lock_guard<std::mutex> _mylock(this->mtx);
        return this->i;
    }
private:
    /* prohibited */
    A(const A& other);
    /* also prohibited */
    A& operator=(const A& other);
};