Multithreading in C++0x part 5: Flexible locking with std::unique_lock<>
Wednesday, 15 July 2009
This is the fifth in a series of blog posts introducing the new C++0x thread library. So far we've looked at the various ways of starting threads in C++0x and protecting shared data with mutexes. See the end of this article for a full set of links to the rest of the series.
In the previous installment we looked at the use of std::lock_guard<>
to simplify the locking and unlocking of a mutex and provide exception
safety. This time we're going to look at the std::lock_guard<>
's
companion class template std::unique_lock<>
. At
the most basic level you use it like std::lock_guard<>
— pass a mutex to the constructor to acquire a lock, and the
mutex is unlocked in the destructor — but if that's all you're
doing then you really ought to use std::lock_guard<>
instead. The benefit to using std::unique_lock<>
comes from two things:
- you can transfer ownership of the lock between instances, and
- the
std::unique_lock<>
object does not have to own the lock on the mutex it is associated with.
Let's take a look at each of these in turn, starting with transferring ownership.
Transferring ownership of a mutex lock
between std::unique_lock<>
instances
There are several consequences to being able to transfer ownership
of a mutex lock
between std::unique_lock<>
instances: you can return a lock from a function, you can store
locks in standard containers, and so forth.
For example, you can write a simple function that acquires a lock on an internal mutex:
std::unique_lock<std::mutex> acquire_lock() { static std::mutex m; return std::unique_lock<std::mutex>(m); }
The ability to transfer lock ownership between instances also provides an easy way to write classes that are themselves movable, but hold a lock internally, such as the following:
class data_to_protect { public: void some_operation(); void other_operation(); }; class data_handle { private: data_to_protect* ptr; std::unique_lock<std::mutex> lk; friend data_handle lock_data(); data_handle(data_to_protect* ptr_,std::unique_lock<std::mutex> lk_): ptr(ptr_),lk(lk_) {} public: data_handle(data_handle && other): ptr(other.ptr),lk(std::move(other.lk)) {} data_handle& operator=(data_handle && other) { if(&other != this) { ptr=other.ptr; lk=std::move(other.lk); other.ptr=0; } return *this; } void do_op() { ptr->some_operation(); } void do_other_op() { ptr->other_operation(); } }; data_handle lock_data() { static std::mutex m; static data_to_protect the_data; std::unique_lock<std::mutex> lk(m); return data_handle(&the_data,std::move(lk)); } int main() { data_handle dh=lock_data(); // lock acquired dh.do_op(); // lock still held dh.do_other_op(); // lock still held data_handle dh2; dh2=std::move(dh); // transfer lock to other handle dh2.do_op(); // lock still held } // lock released
In this case, the function lock_data()
acquires a lock
on the mutex used to protect the data, and then transfers that along
with a pointer to the data into the data_handle
. This
lock is then held by the data_handle
until the handle
is destroyed, allowing multiple operations to be done on the data
without the lock being released. Because
the std::unique_lock<>
is movable, it is easy to make data_handle
movable too,
which is necessary to return it from lock_data
.
Though the ability to transfer ownership between instances is
useful, it is by no means as useful as the simple ability to be able
to manage the ownership of the lock separately from the lifetime of
the std::unique_lock<>
instance.
Explicit locking and unlocking a mutex with
a std::unique_lock<>
As we saw
in part
4 of this
series, std::lock_guard<>
is very strict on lock ownership — it owns the lock from
construction to destruction, with no room for
manoeuvre. std::unique_lock<>
is rather lax in comparison. As well as acquiring a lock in the
constructor as
for std::lock_guard<>
,
you can:
- construct an instance without an associated mutex at all (with the default constructor);
- construct an instance with an associated mutex, but leave the mutex unlocked (with the deferred-locking constructor);
- construct an instance that tries to lock a mutex, but leaves it unlocked if the lock failed (with the try-lock constructor);
- if you have a mutex that supports locking with a timeout (such
as
std::timed_mutex
) then you can construct an instance that tries to acquire a lock for either a specified time period or until a specified point in time, and leaves the mutex unlocked if the timeout is reached; - lock the associated mutex if
the
std::unique_lock<>
instance doesn't currently own the lock (with thelock()
member function); - try and acquire lock the associated mutex if
the
std::unique_lock<>
instance doesn't currently own the lock (possibly with a timeout, if the mutex supports it) (with thetry_lock()
,try_lock_for()
andtry_lock_until()
member functions); - unlock the associated mutex if
the
std::unique_lock<>
does currently own the lock (with theunlock()
member function); - check whether the instance owns the lock (by calling
the
owns_lock()
member function; - release the association of the instance with the mutex, leaving
the mutex in whatever state it is currently (locked or unlocked)
(with
the
release()
member function); and - transfer ownership between instances, as described above.
As you can
see, std::unique_lock<>
is quite flexible: it gives you complete control over the underlying
mutex, and actually meets all the requirements for
a Lockable object itself. You can thus have
a std::unique_lock<std::unique_lock<std::mutex>>
if you really want to! However, even with all this flexibility it
still gives you exception safety: if the lock is held when the
object is destroyed, it is released in the destructor.
std::unique_lock<>
and condition variables
One place where the flexibility
of std::unique_lock<>
is used is
with std::condition_variable
. std::condition_variable
provides an implementation of a condition variable, which
allows a thread to wait until it has been notified that a certain
condition is true. When waiting you must pass in
a std::unique_lock<>
instance that owns a lock on the mutex protecting the data related to
the condition. The condition variable uses the flexibility
of std::unique_lock<>
to unlock the mutex whilst it is waiting, and then lock it again
before returning to the caller. This enables other threads to access
the protected data whilst the thread is blocked. I will expand upon
this in a later part of the series.
Other uses for flexible locking
The key benefit of the flexible locking is that the lifetime of the lock object is independent from the time over which the lock is held. This means that you can unlock the mutex before the end of a function is reached if certain conditions are met, or unlock it whilst a time-consuming operation is performed (such as waiting on a condition variable as described above) and then lock the mutex again once the time-consuming operation is complete. Both these choices are embodiments of the common advice to hold a lock for the minimum length of time possible without sacrificing exception safety when the lock is held, and without having to write convoluted code to get the lifetime of the lock object to match the time for which the lock is required.
For example, in the following code snippet the mutex is unlocked
across the time-consuming load_strings()
operation,
even though it must be held either side to access
the strings_to_process
variable:
std::mutex m; std::vector<std::string> strings_to_process; void update_strings() { std::unique_lock<std::mutex> lk(m); if(strings_to_process.empty()) { lk.unlock(); std::vector<std::string> local_strings=load_strings(); lk.lock(); strings_to_process.insert(strings_to_process.end(), local_strings.begin(),local_strings.end()); } }
Next time
Next time we'll look at the use of the new std::lock()
and std::try_lock()
function
templates to avoid deadlock when acquiring locks on multiple mutexes.
Subscribe to the RSS feed or email newsletter for this blog to be sure you don't miss the rest of the series.
Try it out
If you're using Microsoft Visual Studio 2008 or g++ 4.3 or 4.4 on
Ubuntu Linux you can try out the examples from this series using our
just::thread
implementation of the new C++0x thread library. Get your copy
today.
Multithreading in C++0x Series
Here are the posts in this series so far:
- Multithreading in C++0x Part 1: Starting Threads
- Multithreading in C++0x Part 2: Starting Threads with Function Objects and Arguments
- Multithreading in C++0x Part 3: Starting Threads with Member Functions and Reference Arguments
- Multithreading in C++0x Part 4: Protecting Shared Data
- Multithreading
in C++0x Part 5: Flexible locking
with
std::unique_lock<>
- Multithreading in C++0x part 6: Lazy initialization and double-checked locking with atomics
- Multithreading in C++0x part 7: Locking multiple mutexes without deadlock
- Multithreading in C++0x part 8: Futures, Promises and Asynchronous Function Calls
Posted by Anthony Williams
[/ threading /] permanent link
Tags: concurrency, multithreading, C++0x, thread
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
3 Comments
We had all been looking for this website as it can give you free cash online to your account of paypal.
Two linkage errors in Visual Studio 12 - 1. inside the private Ctor : data_handle(data_to_protect* ptr_,std::unique_lock<std::mutex> lk_), the unique_lock doesn't accept lk(lk_), which means a regular copy Ctor is not available. I replaced it with lk(move(lk)) and that was fixed. 2. in order to support in main(): data_handle dh2; A public default Ctor was added and it fixed the bug.
Is std::mutex statically initializable? Even so, I don't think it will be initialized statically in the way you've written the code. Would you mind explaining?