How do I handle mutexes in movable types in C ++?

By design, std::mutex does not move or copy. This means that the class A that contains the mutex will not receive the default-move constructor.

How to make this type A movable in a thread-safe manner?

+58
c ++ mutex move-constructor
May 01 '15 at 11:41
source share
5 answers

Let's start with the code:

 class A { using MutexType = std::mutex; using ReadLock = std::unique_lock<MutexType>; using WriteLock = std::unique_lock<MutexType>; mutable MutexType mut_; std::string field1_; std::string field2_; public: ... 

I added some pretty far-fetched type aliases that we will not use in C ++ 11, but have become much more useful in C ++ 14. Be patient, we will get there.

Your question comes down to the following:

How to write a move constructor and move an assignment operator for this class?

We will start with the move constructor.

Move constructor

Note that the mutex element is mutable . Strictly speaking, this is not necessary for participants in the movement, but I assume that you also want to copy participants. If this is not the case, there is no need to make a mutable .

When building A you do not need to block this->mut_ . But you need to block mut_ object you are building from (move or copy). This can be done like this:

  A(A&& a) { WriteLock rhs_lk(a.mut_); field1_ = std::move(a.field1_); field2_ = std::move(a.field2_); } 

Note that we first had to construct this elements by default, and then assign their values ​​only after a.mut_ locked.

Move destination

The move assignment operator is significantly more complicated because you do not know if any other stream refers to the lhs or rhs of the assignment expression. In general, you need to protect yourself from the following scenario:

 // Thread 1 x = std::move(y); // Thread 2 y = std::move(x); 

Here is the move destination statement that correctly protects the scenario described above:

  A& operator=(A&& a) { if (this != &a) { WriteLock lhs_lk(mut_, std::defer_lock); WriteLock rhs_lk(a.mut_, std::defer_lock); std::lock(lhs_lk, rhs_lk); field1_ = std::move(a.field1_); field2_ = std::move(a.field2_); } return *this; } 

Note that std::lock(m1, m2) should be used to lock two mutexes, and not just lock them one by one. If you block them one by one, then when two threads assign two objects in the opposite order, as shown above, you can get a dead end. The point of std::lock is to avoid this deadlock.

Copy constructor

You did not ask about the members of the copy, but now we can talk about them (if not you, someone will need them).

  A(const A& a) { ReadLock rhs_lk(a.mut_); field1_ = a.field1_; field2_ = a.field2_; } 

The copy constructor is similar to the move constructor, except that the WriteLock alias is used instead of ReadLock . Currently, both of these aliases are std::unique_lock<std::mutex> , and therefore this really does not matter.

But in C ++ 14 you will have the opportunity to say the following:

  using MutexType = std::shared_timed_mutex; using ReadLock = std::shared_lock<MutexType>; using WriteLock = std::unique_lock<MutexType>; 

It may be optimization, but not definitely. You will need to measure to determine if it is. But with this change, you can copy a construct from the same number in multiple threads at the same time. The C ++ 11 solution forces you to make such streams sequential, even if rhs does not change.

Copy Destination

For completeness, here is the copy assignment operator, which should be reasonably clear after reading the rest:

  A& operator=(const A& a) { if (this != &a) { WriteLock lhs_lk(mut_, std::defer_lock); ReadLock rhs_lk(a.mut_, std::defer_lock); std::lock(lhs_lk, rhs_lk); field1_ = a.field1_; field2_ = a.field2_; } return *this; } 

Etc.

Any other members or free functions that access state A should also be protected if you expect multiple threads to be able to call them immediately. For example, here is swap :

  friend void swap(A& x, A& y) { if (&x != &y) { WriteLock lhs_lk(x.mut_, std::defer_lock); WriteLock rhs_lk(y.mut_, std::defer_lock); std::lock(lhs_lk, rhs_lk); using std::swap; swap(x.field1_, y.field1_); swap(x.field2_, y.field2_); } } 

Please note that if you just depend on std::swap for the job, the lock will be inaccurate in detail, lock and unlock between the three moves that std::swap will execute internally.

In fact, thinking about swap may give you an idea of ​​the API that might be required to provide thread-safe A , which in general would be different from the “unsafe” API, due to the “drill down lock” problem.

Also pay attention to the need for protection against "self-exchange". "self-swap" must be non-op. Without a self-test, it recursively blocks the same mutex. This could also be solved without a self-test using std::recursive_mutex for MutexType .

Update

In the comments below, Yakk is rather unhappy with the need to build things in copy and move constructs by default (and he has a point). If you are decisive enough about this problem, so that you are ready to spend memory on it, you can avoid it like this:

  • Add all the types of locks that you need as data items. These members must be in front of the protected data:

     mutable MutexType mut_; ReadLock read_lock_; WriteLock write_lock_; // ... other data members ... 
  • And then in the constructors (for example, in the copy constructor) do the following:

     A(const A& a) : read_lock_(a.mut_) , field1_(a.field1_) , field2_(a.field2_) { read_lock_.unlock(); } 

Oops, Jakk deleted his comment before I had the opportunity to complete this update. But he deserves recognition for pushing this issue and making a decision in this answer.

Update 2

And dyp came up with this good suggestion:

  A(const A& a) : A(a, ReadLock(a.mut_)) {} private: A(const A& a, ReadLock rhs_lk) : field1_(a.field1_) , field2_(a.field2_) {} 
+72
May 01 '15 at 02:20
source share

Given that this does not seem like a good, clean and simple way to answer this question - I think Anton’s decision is correct, but it is decidedly controversial, if there is no better answer, I would recommend putting such a class in a heap and looking through it through std::unique_ptr :

 auto a = std::make_unique<A>(); 

Now this is a fully mobile type, and anyone who has an internal mutex lock while holding an action is still safe, even if they are discussing whether it’s good to do it

If you need copy semantics, just use

 auto a2 = std::make_shared<A>(); 
+4
May 01 '15 at 12:45
source share

This is an inverted answer. Instead of introducing "these objects need to be synchronized" as the base of the type, instead enter it under any type.

You deal with a synchronized object in different ways. One of the serious problems is that you have to worry about deadlocks (blocking multiple objects). It also should never be your “standard version of an object”: synchronized objects are for objects that will be in conflict, and your goal should be to minimize the conflict between threads, rather than sweeping it under the carpet.

But sync objects are still useful. Instead of inheriting from a synchronizer, we can write a class that wraps an arbitrary type during synchronization. Users must go through several hoops to perform operations on the object now that they are synchronized, but they are not limited to any limited set of operations with limited manual control for the object. They can be several operations on an object in one or work with several objects.

Here is a synchronized wrapper around an arbitrary type T :

 template<class T> struct synchronized { template<class F> auto read(F&& f) const&->std::result_of_t<F(T const&)> { return access(std::forward<F>(f), *this); } template<class F> auto read(F&& f) &&->std::result_of_t<F(T&&)> { return access(std::forward<F>(f), std::move(*this)); } template<class F> auto write(F&& f)->std::result_of_t<F(T&)> { return access(std::forward<F>(f), *this); } // uses `const` ness of Syncs to determine access: template<class F, class... Syncs> friend auto access( F&& f, Syncs&&... syncs )-> std::result_of_t< F(decltype(std::forward<Syncs>(syncs).t)...) > { return access2( std::index_sequence_for<Syncs...>{}, std::forward<F>(f), std::forward<Syncs>(syncs)... ); }; synchronized(synchronized const& o):t(o.read([](T const&o){return o;})){} synchronized(synchronized && o):t(std::move(o).read([](T&&o){return std::move(o);})){} // special member functions: synchronized( T & o ):t(o) {} synchronized( T const& o ):t(o) {} synchronized( T && o ):t(std::move(o)) {} synchronized( T const&& o ):t(std::move(o)) {} synchronized& operator=(T const& o) { write([&](T& t){ t=o; }); return *this; } synchronized& operator=(T && o) { write([&](T& t){ t=std::move(o); }); return *this; } private: template<class X, class S> static auto smart_lock(S const& s) { return std::shared_lock< std::shared_timed_mutex >(sm, X{}); } template<class X, class S> static auto smart_lock(S& s) { return std::unique_lock< std::shared_timed_mutex >(sm, X{}); } template<class L> static void lock(L& lockable) { lockable.lock(); } template<class...Ls> static void lock(Ls&... lockable) { std::lock( lockable... ); } template<size_t...Is, class F, class...Syncs> friend auto access2( std::index_sequence<Is...>, F&&f, Syncs&&...syncs)-> std::result_of_t< F(decltype(std::forward<Syncs>(syncs).t)...) > { auto locks = std::make_tuple( smart_lock<std::defer_lock_t>(syncs)... ); lock( std::get<Is>(locks)... ); return std::forward<F>(f)(std::forward<Syncs>(syncs).t ...); } mutable std::shared_timed_mutex m; T t; }; template<class T> synchronized< T > sync( T&& t ) { return {std::forward<T>(t)}; } 
Features Enabled

C ++ 14 and C ++ 1z.

this suggests that const operations are safe with multiple readers (as the std container implies).

Usage looks like this:

 synchronized<int> x = 7; x.read([&](auto&& v){ std::cout << v << '\n'; }); 

for int with synchronized access.

I would advise synchronized(synchronized const&) having synchronized(synchronized const&) . This is rarely necessary.

If you need synchronized(synchronized const&) , I will be tempted to replace T t; on std::aligned_storage , allowing you to build manual placement and manual destruction. This allows you to properly manage the life cycle.

Other than this, we could copy the source of T , and then read it:

 synchronized(synchronized const& o): t(o.read( [](T const&o){return o;}) ) {} synchronized(synchronized && o): t(std::move(o).read( [](T&&o){return std::move(o);}) ) {} 

for appointment:

 synchronized& operator=(synchronized const& o) { access([](T& lhs, T const& rhs){ lhs = rhs; }, *this, o); return *this; } synchronized& operator=(synchronized && o) { access([](T& lhs, T&& rhs){ lhs = std::move(rhs); }, *this, std::move(o)); return *this; } friend void swap(synchronized& lhs, synchronized& rhs) { access([](T& lhs, T& rhs){ using std::swap; swap(lhs, rhs); }, *this, o); } 

the placement and aligned versions of storage are a bit messy. Most of the access to T will be replaced by the member function T&t() and T const&t()const , except during construction, where you have to jump over some hoops.

Having made a synchronized wrapper instead of part of the class, all we need to make sure is that the class internally respects const as multitasking and writes it in a single-threaded way.

In rare cases, we need a synchronized instance, we go through the hoops, as indicated above.

Sorry for any typos in the above. There are probably some.

A side advantage of the above is that n-ary arbitrary operations on synchronized objects (of the same type) work together, without the need to hard-code it before starting work. Add friend and n-ary synchronized objects of several types to the declaration can work together. I may need to move access from a built-in friend to deal with overloads in this case.

living example

+4
May 01 '15 at 18:43
source share

Using mutexes and C ++ relocation semantics is a great way to safely and efficiently transfer data between threads.

Imagine a “producer” stream that produces batches of strings and provides them to (one or more) consumers. These parties can be represented by an object containing (potentially large) objects std::vector<std::string> . We absolutely want to “move” the internal state of these vectors to our consumers without unnecessary duplication.

You simply recognize the mutex as part of an object that is not part of the state of the object. That is, you do not want to move the mutexes.

The required lock depends on your algorithm or on how generalized your objects are and what range of use you allow.

If you are just moving from a producer object to a common consuming thread object, you might be fine to block only the moved object from .

If this is a more general design, you will need to block both. In this case, you need to think about locking the lock.

If this is a potential problem, use std::lock() to get locks on both mutexes without locking.

http://en.cppreference.com/w/cpp/thread/lock

As a final note, you need to make sure that you understand the semantics of movement. Recall that the displaced from the object remains in a valid, but unknown state. It is possible that a thread that does not perform a move has a good reason for trying to access a moved object when it can find this real, but unknown state.

Again, my producer just beats the lines, and the consumer removes the entire load. In this case, every time the manufacturer tries to add to the vector, he can find the vector non-empty or empty.

In short, if the potential concurrent access to the amount transferred from the object to the record is likely to be OK. If this is a read, then consider why it is normal to read an arbitrary state.

+3
May 01 '15 at
source share

First of all, there must be something wrong with your design if you want to move an object containing a mutex.

But if you decide to do it anyway, you need to create a new mutex in the move constructor, for example:

 // movable struct B{}; class A { B b; std::mutex m; public: A(A&& a) : b(std::move(ab)) // m is default-initialized. { } }; 

This is thread safe because the move constructor can safely assume that its argument is not used anywhere, so argument locking is not required.

+2
May 01 '15 at 12:02
source share



All Articles