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;
}
};
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With