Blog Archive for / 2011 / 08 /
Thread-Safe Copy and Move Constructors
Wednesday, 17 August 2011
This is a guest post by Michael Spertus. Michael is a Distinguished Engineer at Symantec. He is also a C++ Standards Committee member and teaches the graduate C++ sequence at the University of Chicago. He can be contacted at mike_spertus@symantec.com.
This guest column discusses writing thread-safe constructors. As we will see, this is more difficult than it seems. Fortunately, we will also see that C++11 offers a very pretty solution to this problem that nicely illustrates the synergy of the new features introduced in C++11.
The problem
If you have a class that supports locking of objects to serialize access to a given object, you probably want the class' copy constructor and move constructor (if it has one) to lock the source object to get a consistent snapshot of the source object so the destination object isn't messed up if the source changes in the middle of the copy or move.
This isn't nearly as easy as it sounds. In the following class, a
mutex is used to try to enforce the invariant that i_squared
should always be the square of i
.
class A { public: A(_i = 0) { set(_i); } set(int _i) { std::lock_guard<std::mutex> lock(mtx); i = _i; i_squared = i*i; } ... private: std::mutex mtx; int i; int i_squared; };
Unfortunately, the default copy constructor doesn't acquire the
mutex, so in code like the following, f
can copy a "half set" version of a
if another thread modifies a
at the same time.
void f(A &a) { A a2 = a; ... }
First attempt
A naive attempt is to acquire the lock in the constructor body just like in a thread-safe method.
class A { public: A(const A &a) : i(a.i), i_squared(a.i_squared) { std::lock_guard<std::mutex> lock(a.mtx); // Too late! } ... };
Unfortunately, this fares no better
as i
and i_squared
are
copied before we acquire the lock.
Second attempt
One approach would be to simply not lock in the copy constructor at all and just manually lock objects you want to copy:
void f(A &a) { std::lock_guard<std::mutex> lock(a.mtx); A a2 = a; ... }
This approach deserves careful consideration. For classes which
are not usually shared between threads or which need locking granularity
at a different level than their internal operations, managing locks
within the class can be an antipattern. This concern was a primary reason
why C++11 does not have an equivalent to the SynchronizedCollection
wrapper found in Java and C#. For example, synchronized
collections make it easy to inadvertently loop through a collection believing
your code is thread-safe even though the collection could change between
individual operations on the collection during the loop. Of course, if
we decide not to have A
's copy constructor lock, then A::set()
should not lock either.
Still, it remains a very common and useful pattern for classes designed for shared
use to have all their internal operations acquire the
lock (i.e.,
monitors/synchronized classes).
If A
is a synchronized class that locks its methods internally, it would
be very confusing and prone to intermittent errors to still have to manually acquire
the
lock whenever an object is copied or moved. Also, generic
code, which doesn't know about A::mtx
is unlikely to work
properly.
Third attempt
One thing we can do is dispense with member initialization lists in constructors altogether
class A { public: A(const A &a) { std::lock_guard<std::mutex> lock(a.mtx); i = a.i; i_squared = a.i_squared; } ... };
This solution is awkward at best if any bases or members don't have default constructors, have reference type, or are const. It also seems unfair to have to pay an efficiency penalty (for constructing and assigning separately) just because there is no place to put the lock. In practice, I also suspect intermittent errors will creep into large code bases as programmers carelessly add a base or member initializer to the constructor. Finally, it just isn't very satisfying to have to just discard core parts of constructor syntax just because your class is synchronized.
Fourth attempt
Anthony Williams has suggested implementing serialized classes using a wrapper class like:
struct PrivateBaseForA { int i; int i_squared; }; class A: private PrivateBaseForA { mutable std::mutex mtx; public: A(int _i = 0) { set(_i); } void set(int _i) { std::lock_guard<std::mutex> lock(mtx); i = _i; i_squared = _i*_i; } A(const A& other): PrivateBaseForA((std::lock_guard<std::mutex>(other.mtx),other)) {} };
Anthony makes slick use of double-parens to use the comma operator. If you
wanted to avoid this, you could have PrivateBaseForA
's constructor take a lock.
Again, this is not yet a very satisfying solution because writing a wrapper class for every synchronized class just to get a properly locked copy constructor is clumsy and intrusive.
Finally, the C++11 approach
Fortunately, C++11 offers a nice solution that really illustrates how beautifully and powerfully the features of C++11 work together. C++11 supports forwarding one constructor to another, which gives us an opportunity to grab the mutex before any copying or moving takes place:
class A { private: A(const A &a, const std::lock_guard<std::mutex> &) : i(a.i), i_squared(a.i_squared) {} public: A(const A &a) : A(a, std::lock_guard<std::mutex>(a.mtx)) {} ... };
This solution locks the entire constructor, protects against races resulting from forgetting the manual lock, works with generic code, and doesn't require the creation of artificial wrapper classes.
While I think this is clearly the preferred solution, it is not perfect. To begin with, it is more obscure and guru-like than I would wish for such a common situation. Secondly, constructor forwarding is not yet implemented by any major C++ compiler (although the recently approved standard should change that soon). Finally, we lose the benefit of compiler generated copy constructors. If we added an i_cubed field to A that also needed to be kept consistent with i, we might forget to update the private constructor. Perhaps this will be a further opportunity for the next C++ standard (C++1y?). In the meantime, C++11 provides a powerful new solution to the everyday problem of writing thread-safe copy and move constructors.
One final note is to mention that although this article focused on copy constructors, everything applies equally to move constructors. Indeed, any constructor that needs to acquire any lock whatsoever (e.g., a database lock) for its entire duration can apply these techniques.
Posted by Anthony Williams
[/ threading /] permanent link
Tags: multithreading, copying
Stumble It! | Submit to Reddit
| Submit to DZone
If you liked this post, why not subscribe to the RSS feed or Follow me on Twitter? You can also subscribe to this blog by email using the form on the left.
Design and Content Copyright © 2005-2025 Just Software Solutions Ltd. All rights reserved. | Privacy Policy