views:

137

answers:

4

Hi,

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

};
+1  A: 

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!

smilingthax
I'm sorry, I corrected the `lock_guard`s. I believe that in order to be in a consistent state, I think the safest way is to lock the whole object rather than using thread-safe getters.
ipapadop
Sure. But if e.g. one thread-safe getter would be enough for a certain class you can avoid the additional complexity (in terms of what-could-happen).
smilingthax
A: 

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.

DeadMG
Yes, boost does it as well - I just wrote it in C++0x provided facilities that are supported by some compilers already (GCC 4.3+, MSVC 10 and some others). It will lead to less-than-optimal performance, but it is the only way if you do not have a lock-free data structure.
ipapadop
@ipapadop: MSVC10 doesn't support Standard threading facilities. They rolled their own (the ConcRT) for that release. The performance I meant is that threading of an application must be designed in before-hand. What if somebody who wishes to use your object isn't multi-threaded, or it's otherwise unnecessary? The fact is that you, as an object provider, have no idea when, where, or even if the object user requires synchronization unless threading is an explicit part of the purpose of the object.
DeadMG
It's used in my own multithreaded code. You are right about MSVC - I forgot I changed to using Intel Compiler + TBB on Windows which they offer all that. I can always disable the mutex by passing a null_mutex that has null_mutex::lock() and null_mutex::unlock() that do nothing.
ipapadop
A: 

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);
};
Justin
I do not wish to use the get() function - mainly because it is not generic enough; what happens if I have multiple values (look also at my note)? You are also making two mistakes: your version would deadlock in a self-assignment case and mutex.lock() can throw an exception in your destructor. Finally, I am declaring mutex as mutable, so the const does not create any problems.
ipapadop
@ipapdop re init: you can still initialize correctly with one acquisition if you create an object to pass as an argument. re deadlocks: it's good to create a second template parameter for the type of lock you'll use, or base classes which are lockable. subclasses with locks are often ideal because you'd rather protect the object's vulnerable state, rather than the members within restricted scopes. attempting to ensure thread safety using a basic mutex for an arbitrary `T` is too often tedious and error prone.
Justin
(cont) a singular interface (as a generic pattern) is very susceptable to deadlocks (some of which will be intensely difficult to reproduce). your example is trivial in comparison to real world usage; a recursive mutex is more practical generalized object lock. it's also best to write/use a few additional objects to convey and enforce your intentions at every stage within the implementation. using `mutable` will only save you from rewriting existing code today. if thread safety and program correctness is your primary concern, i encourage you to omit it.
Justin
As a general rule, I avoid recursive locks as IMO they just hide a bad use of locks. Yes it is a trivial example but I don't think I should post the whole thing - however, it is very similar to what I have so 1) I cannot use atomic operations and 2) it has to conform to common practice where a get() is a const. I do not see why mutable is such a bad practice, since it has nothing to do with the internal state of the object - it is there to allow the mutex to enforce thread-safety.
ipapadop
A: 

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.

Martin
I do not particularly care about the lifetime - this is a different issue, completely orthogonal to have a thread-safe object. I am using the pattern above to protect access to shared resources that are not thread-safe and in addition inherently not scalable (so I do not care about performance and the shared resource outlives the threads). The wrong thing here is to have an internal inconsistent state, not getting different values. Thanks for the reminder on the move operations though.
ipapadop
I wonder what special resources you have here that are *not scalable*, *outlive the threads* and still need to be copied? Or do you have "wrapper objects" to your resources and need to copy these?
Martin
Simple use-case (not mine, but you get the idea): a queue that threads put objects in it. Every now and then, a thread comes and copies that queue (does not clear it) and dumps it into a file. The other threads continue to use the initial queue and continue to fill it. You now have a snapshot system.
ipapadop