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

发布于 2024-09-29 05:44:26 字数 1541 浏览 0 评论 0原文

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

#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;
  }

};

Let's say that we want to make class A thread-safe using an std::mutex. I am having my copy constructor and assignment operator similarly to the code below:

#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;
  }
};

I do not think that it has any problems, other than the possibility of other to be destroyed by another thread before being copied, which I can deal with.

My issue is that I haven't seen this pattern anywhere, so I do not know if people just haven't had a need for that or that it is plainly wrong for reasons I currently don't see.

Thanks

NOTES:

This is just an example. I can have an arbitrary number of member variables of any type, it does not have to be just an int.

After Martin York's comment for possible deadlocking, this is an updated version that uses copy-and-swap (copy elision is also possible, but it wouldn't handle efficiently the self-assignment case).

I also changed int to T, so people cannot assume that it is a 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;
  }

};

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

月亮坠入山谷 2024-10-06 05:44:26

老问题,新答案:

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

  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),有一个新的 标头提供读/写锁定功能,可能提供性能提升(性能对于特定用例需要进行测试来确认这一点)。下面是它的使用方式:

#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可以轻松完成此操作。当您需要以“独占模式”锁定互斥锁时,可以根据需要使用 unique_locklock_guard,并且方式与您相同可以将此功能与 std::mutex 一起使用。

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

Old question, new answer:

Imho, a better way to deal with the dead-lock problem of the original copy assignment operator is:

  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;
  }

I.e. use std::lock(L1, L2) to simultaneously lock the two mutexes without fear of deadlock. This is likely to be higher performance than the copy/swap idiom, especially if the member data consists of std::vector, std::string, or types that contain vectors and/or strings.

In C++1y (we hope y is 4), there is a new <shared_mutex> header providing read/write lock capability which may provide a performance boost (performance testing would be necessary for specific use cases to confirm that). Here is how it would be used:

#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;
  }
};

I.e. this is very similar to the original code (modified to use std::lock as I've done above). But the member mutex type is now std::shared_mutex instead of std::mutex. And when protecting a const A (and assuming no mutable members besides the mutex), one need only lock the mutex in "shared mode". This is easily done using shared_lock<shared_mutex>. When you need to lock the mutex in "exclusive mode", you can use unique_lock<shared_mutex>, or lock_guard<shared_mutex> as appropriate, and in the same manner as you would have used this facilities with std::mutex.

In particular note that now many threads can simultaneously copy construct from the same A, or even copy assign from the same A. But only one thread can still copy assign to the same A at a time.

素年丶 2024-10-06 05:44:26

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

  • 如果从多个线程访问对象,那么您(另外)必须管理对象的生命周期,而不能从对象内部管理对象的生命周期。
  • 对于生命周期管理,您已经需要至少一个对象外部锁,因此最好使用它。
  • 此方案仅对单 get() 对象有意义 - 如果您的对象具有多个(多个成员或多个)get() 函数,则从该对象可能/将会导致数据不一致。

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

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

Ignoring all implementation details, the reason you don't see this pattern is because it is very likely that you are locking on the wrong abstraction level.

  • If the objects are accessed from multiple threads, then you (additionally) have to manage the lifetime of the objects, which cannot be managed from within the objects.
  • For lifetime management you already need at least one object-external lock, so better use that.
  • This scheme only makes sense for single-get() objects -- if your object has more (than one member and more) than one get() function, then reading from the object can/will result in inconsistent data.

Getting correct multithreaded code isn't just a matter of makeing sure nothing "crashes" and single objects stay in a consistent state. And if you (think you) need the above scheme you may think you're save when your app is still doing-the-wrong-thing.

As for implementation details: Since you are already using C++0x for this, you also should implement appropriately defined move operations.

屌丝范 2024-10-06 05:44:26

我不是这方面的权威,因为多线程很棘手,但到目前为止看起来还不错。
顺便说一句,您可能的意思

std::lock_guard<std::mutex>

是在复制因子中:

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

确保 other 线程安全的另一种方法是仅使用“安全”getters 来访问它,尽管当多个 getters 时,这不会按预期运行。叫。但是,请注意引用!

I'm not an authority on this, because multi-threading is tricky, but it looks fine so far.
BTW, you probably meant

std::lock_guard<std::mutex>

and in the copy-ctor:

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

Another way to ensure thread-safety for other is only using 'safe' getters to access it, although this would not behave as expected when multiple getters are called. But, beware of references!

贵在坚持 2024-10-06 05:44:26

您实际上并没有看到它,因为标准线程设施非常新,而且我不知道有哪个编译器支持它们 - 您需要进一步寻找 boost::thread 示例。另外,无端使用同步可能会导致性能不佳,但这只是我的观点。

You don't really see it because the Standard threading facilities are exceedingly new and I don't know of a single compiler that supports them - you'd get further looking for boost::thread examples. Also, your gratuitous use of synchronization will likely lead to poor performance, but that's just my opinion.

单挑你×的.吻 2024-10-06 05:44:26

这是更正确的,但并不完全可靠:

#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);
};

this is more correct, but not entirely robust:

#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);
};
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文