Just Software Solutions

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:

  1. you can transfer ownership of the lock between instances, and
  2. 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:

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 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:

Posted by Anthony Williams
[/ threading /] permanent link
Tags: , , ,
Stumble It! stumbleupon logo | Submit to Reddit reddit logo | Submit to DZone dzone logo

Comment on this post

If you liked this post, why not subscribe to the RSS feed RSS feed or Follow me on Twitter? You can also subscribe to this blog by email using the form on the left.

3 Comments

We had all been looking for this website as it can give you free cash online to your account of paypal.

by free money paypal at 15:00:33 on Monday, 21 January 2019

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.

by oreng at 15:00:33 on Monday, 21 January 2019

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?

by Sohail at 15:00:33 on Monday, 21 January 2019

Add your comment

Your name:

Email address:

Your comment:

Design and Content Copyright © 2005-2024 Just Software Solutions Ltd. All rights reserved. | Privacy Policy