120

I have a multithreaded app that has to read some data often, and occasionally that data is updated. Right now a mutex keeps access to that data safe, but it's expensive because I would like multiple threads to be able to read simultaneously, and only lock them out when an update is needed (the updating thread could wait for the other threads to finish).

I think this is what boost::shared_mutex is supposed to do, but I'm not clear on how to use it, and haven't found a clear example.

Does anyone have a simple example I could use to get started?

2

6 Answers 6

176

1800 INFORMATION is more or less correct, but there are a few issues I wanted to correct.

boost::shared_mutex _access;
void reader()
{
  boost::shared_lock< boost::shared_mutex > lock(_access);
  // do work here, without anyone having exclusive access
}

void conditional_writer()
{
  boost::upgrade_lock< boost::shared_mutex > lock(_access);
  // do work here, without anyone having exclusive access

  if (something) {
    boost::upgrade_to_unique_lock< boost::shared_mutex > uniqueLock(lock);
    // do work here, but now you have exclusive access
  }

  // do more work here, without anyone having exclusive access
}

void unconditional_writer()
{
  boost::unique_lock< boost::shared_mutex > lock(_access);
  // do work here, with exclusive access
}

Also Note, unlike a shared_lock, only a single thread can acquire an upgrade_lock at one time, even when it isn't upgraded (which I thought was awkward when I ran into it). So, if all your readers are conditional writers, you need to find another solution.

Sign up to request clarification or add additional context in comments.

11 Comments

Just to comment on "another solution". When all my readers where conditional writers, what I did was have them always acquire a shared_lock, and when I needed to upgrade to write privilages, I would .unlock() the reader lock and acquire a new unique_lock. This will complicate the logic of your application, and there is now a window of opportunity for other writers to change the state from when you first read.
Shouldn't the line boost::unique_lock< boost::shared_mutex > lock(lock); read boost::unique_lock< boost::shared_mutex > lock( _access ); ?
That last caveat is very strange. If only one thread can hold an upgrade_lock at a time, what's the difference between an upgrade_lock and a unique_lock?
@Ken I was not very clear, but the benefit of upgrade_lock is it does not block if there are currently some shared_locks acquired (at least not until you upgrade to unique). However, the second thread to try and acquire an upgrade_lock will block, even if the first has not upgraded to unique, which I had not expected.
This is a known boost issue. It seems to be resolved at boost 1.50 beta: svn.boost.org/trac/boost/ticket/5516
|
105

It looks like you would do something like this:

boost::shared_mutex _access;
void reader()
{
  // get shared access
  boost::shared_lock<boost::shared_mutex> lock(_access);

  // now we have shared access
}

void writer()
{
  // get upgradable access
  boost::upgrade_lock<boost::shared_mutex> lock(_access);

  // get exclusive access
  boost::upgrade_to_unique_lock<boost::shared_mutex> uniqueLock(lock);
  // now we have exclusive access
}

7 Comments

This is my first time using boost, and I'm a C++ newbie, so maybe there's something I'm missing -- but in my own code, I had to specify the type, like so: boost::shared_lock<shared_mutex> lock(_access);
I'm trying to use this myself but I'm getting an error. missing template arguments before 'lock'. Any ideas?
@shaz Those are scoped, but you can release them early with .unlock() if you need.
I've added the missing template arguments.
@raaj you can get the upgrade_lock, but upgrading to a unique lock will block until the shared_lock is released
|
51

Since C++ 17 (VS2015) you can use the standard for read-write locks:

#include <shared_mutex>

typedef std::shared_mutex Lock;
typedef std::unique_lock< Lock > WriteLock;
typedef std::shared_lock< Lock > ReadLock;

Lock myLock;


void ReadFunction()
{
    ReadLock r_lock(myLock);
    //Do reader stuff
}

void WriteFunction()
{
     WriteLock w_lock(myLock);
     //Do writer stuff
}

For older version, you can use boost with the same syntax:

#include <boost/thread/locks.hpp>
#include <boost/thread/shared_mutex.hpp>

typedef boost::shared_mutex Lock;
typedef boost::unique_lock< Lock >  WriteLock;
typedef boost::shared_lock< Lock >  ReadLock;

3 Comments

I'd also say typedef boost::unique_lock< Lock > WriteLock; typedef boost::shared_lock< Lock > ReadLock;.
Don't need to include the whole thread.hpp . If you just need the locks, include the locks. It's not an internal implementation. Keep the includes to the minimal.
Definitely the simplest implementation but I think it's confusing to refer to both mutexes and locks as Locks. A mutex is a mutex, a lock is something that maintains it in a locked state.
18

Just to add some more empirical info, I have been investigating the whole issue of upgradable locks, and Example for boost shared_mutex (multiple reads/one write)? is a good answer adding the important info that only one thread can have an upgrade_lock even if it is not upgraded, that is important as it means you cannot upgrade from a shared lock to a unique lock without releasing the shared lock first. (This has been discussed elsewhere but the most interesting thread is here http://thread.gmane.org/gmane.comp.lib.boost.devel/214394)

However I did find an important (undocumented) difference between a thread waiting for an upgrade to a lock (ie needs to wait for all readers to release the shared lock) and a writer lock waiting for the same thing (ie a unique_lock).

  1. The thread that is waiting for a unique_lock on the shared_mutex blocks any new readers coming in, they have to wait for the writers request. This ensures readers do not starve writers (however I believe writers could starve readers).

  2. The thread that is waiting for an upgradeable_lock to upgrade allows other threads to get a shared lock, so this thread could be starved if readers are very frequent.

This is an important issue to consider, and probably should be documented.

1 Comment

The Terekhov algorithm ensures that in 1., the writer cannot starve the readers. See this. But 2. is true. An upgrade_lock does not guarantee fairness. See this.
2

Use a semaphore with a count that is equal to the number of readers. Let each reader take one count of the semaphore in order to read, that way they can all read at the same time. Then let the writer take ALL the semaphore counts prior to writing. This causes the writer to wait for all reads to finish and then block out reads while writing.

2 Comments

(1) How do you make a writer decrement the count by an arbitrary amount atomically? (2) If the writer does somehow decrement the count to zero, how does it wait for already-running readers to finish before writing?
Bad idea : If two writers try to access simultaneously, you can have a deadlock.
2

Great response by Jim Morris, I stumbled upon this and it took me a while to figure. Here is some simple code that shows that after submitting a "request" for a unique_lock boost (version 1.54) blocks all shared_lock requests. This is very interesting as it seems to me that choosing between unique_lock and upgradeable_lock allows if we want write priority or no priority.

Also (1) in Jim Morris's post seems to contradict this: Boost shared_lock. Read preferred?

#include <iostream>
#include <boost/thread.hpp>

using namespace std;

typedef boost::shared_mutex Lock;
typedef boost::unique_lock< Lock > UniqueLock;
typedef boost::shared_lock< Lock > SharedLock;

Lock tempLock;

void main2() {
    cout << "10" << endl;
    UniqueLock lock2(tempLock); // (2) queue for a unique lock
    cout << "11" << endl;
    boost::this_thread::sleep(boost::posix_time::seconds(1));
    lock2.unlock();
}

void main() {
    cout << "1" << endl;
    SharedLock lock1(tempLock); // (1) aquire a shared lock
    cout << "2" << endl;
    boost::thread tempThread(main2);
    cout << "3" << endl;
    boost::this_thread::sleep(boost::posix_time::seconds(3));
    cout << "4" << endl;
    SharedLock lock3(tempLock); // (3) try getting antoher shared lock, deadlock here
    cout << "5" << endl;
    lock1.unlock();
    lock3.unlock();
}

3 Comments

I'm actually having trouble figuring out why the above code deadlocks while the code in [stackoverflow.com/questions/12082405/… works.
It actually deadlocks in (2), not in (3), because (2) is waiting for (1) to release its lock. Remember: to get a unique lock, you need to wait for all the existing shared locks to finish.
@JonesV, even if (2) waits for all shared locks to finish, it wouldn't be a deadlock because it's a different thread than the one which acquired (1), if line (3) didn't exist, the program would finish with no deadlocks.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.