Cross-platform up- and downgradable read/write lock - c++

I try to turn some central data structure of a large codebase multithreaded.
The access interfaces were changed to represent read/write locks, which may be up- and downgraded:
Before:
Container& container = state.getContainer();
auto value = container.find( "foo" )->bar;
container.clear();
Now:
ReadContainerLock container = state.getContainer();
auto value = container.find( "foo" )->bar;
{
// Upgrade read lock to write lock
WriteContainerLock write = state.upgrade( container );
write.clear();
} // Downgrades write lock to read lock
Using an actual std::mutex for the locking (instead of r/w implementation) works fine but brings no performance benefit (actually degrades runtime).
Actual changing data is relatively rare, so it seems very desirable to go with the read/write concept. The big issue now is that I cannot seem to find any library, which implements the read/write concept and supports upgrade and downgrade and works on Windows, OSX and Linux alike.
Boost has BOOST_THREAD_PROVIDES_SHARED_MUTEX_UPWARDS_CONVERSIONS but does not seem to support downgrading (blocking) atomic upgrading from shared to unique.
Is there any library out there, that supports the desired feature set?
EDIT:
Sorry for being unclear. Of course I mean multiple-readers/single-writer lock semantic.

The question has changed since I answered. As the previous answer is still useful, I will leave it up.
The new question seems to be "I want a (general purpose) reader writer lock where any reader can be upgraded to a writer atomically".
This cannot be done without deadlocks, or the ability to roll back operations (transactional reads), which is far from general-purpose.
Suppose you have Alice and Bob. Both want to read for a while, then they both want to write.
Alice and Bob both get a read lock. They then upgrade to a write lock. Neither can progress, because a write lock cannot be acquired while a read lock is acquired. You cannot unlock the read lock, because then the state Alice read while read locked may not be consistent with the state after the write lock is acquired.
This can only be solved with the possibility the read->write upgrade can fail, or the ability to rollback all operations in a read (so Alice can "unread", Bob can advance, then Alice can re-read and try to get the write lock).
Writing type-safe transactional code isn't really supported in C++. You can do it manually, but beyond simple cases it is error prone. Other forms of transactional rollbacks can also be used. None of them are general purpose reader-writer locks.

You can roll your own. If the states are R, U, W and {} (read, upgradable, write and no lock), these are transitions you can easily support:
{} -> R|U|W
R|U|W -> {}
U->W
W->U
U->R
and implied by the above:
W->R
which I think satisifies your requirements.
The "missing" transition is R->U, which is what lets us have multiple-readers safely. At most one reader (the upgrade reader) has the right to upgrade to write without releasing their read lock. While they are in that upgrade state they do not block other threads from reading (but they do block other threads from writing).
Here is a sketch. There is a shared_mutex A; and a mutex B;.
B represents the right to upgrade to write and the right to read while you hold it. All writers also hold a B, so you cannot both have the right to upgrade to write while someone else has the right to write.
Transitions look like:
{}->R = read(A)
{}->W = lock(B) then write(A)
{}->U = lock(B)
U->W = write(A)
W->U = unwrite(A)
U->R = read(A) then unlock(B)
W->R = W->U->R
R->{} = unread(A)
W->{} = unwrite(A) then unlock(B)
U->{} = unlock(B)
This simply requires std::shared_mutex and std::mutex, and a bit of boilerplate to write up the locks and the transitions.
If you want to be able to spawn a write lock while the upgrade lock "remains in scope" extra work needs to be done to "pass the upgrade lock back to the read lock".
Here are some bonus try transitions, inspired by #HowardHinnat below:
R->try U = return try_lock(B) && unread(A)
R->try W = return R->try U->W
Here is an upgradable_mutex with no try operations:
struct upgradeable_mutex {
std::mutex u;
std::shared_timed_mutex s;
enum class state {
unlocked,
shared,
aspiring,
unique
};
// one step at a time:
template<state start, state finish>
void transition_up() {
transition_up<start, (state)((int)finish-1)>();
transition_up<(state)((int)finish-1), finish>();
}
// one step at a time:
template<state start, state finish>
void transition_down() {
transition_down<start, (state)((int)start-1)>();
transition_down<(state)((int)start-1), finish>();
}
void lock();
void unlock();
void lock_shared();
void unlock_shared();
void lock_aspiring();
void unlock_aspiring();
void aspiring_to_unique();
void unique_to_aspiring();
void aspiring_to_shared();
void unique_to_shared();
};
template<>
void upgradeable_mutex::transition_up<
upgradeable_mutex::state::unlocked, upgradeable_mutex::state::shared
>
() {
s.lock_shared();
}
template<>
void upgradeable_mutex::transition_down<
upgradeable_mutex::state::shared, upgradeable_mutex::state::unlocked
>
() {
s.unlock_shared();
}
template<>
void upgradeable_mutex::transition_up<
upgradeable_mutex::state::unlocked, upgradeable_mutex::state::aspiring
>
() {
u.lock();
}
template<>
void upgradeable_mutex::transition_down<
upgradeable_mutex::state::aspiring, upgradeable_mutex::state::unlocked
>
() {
u.unlock();
}
template<>
void upgradeable_mutex::transition_up<
upgradeable_mutex::state::aspiring, upgradeable_mutex::state::unique
>
() {
s.lock();
}
template<>
void upgradeable_mutex::transition_down<
upgradeable_mutex::state::unique, upgradeable_mutex::state::aspiring
>
() {
s.unlock();
}
template<>
void upgradeable_mutex::transition_down<
upgradeable_mutex::state::aspiring, upgradeable_mutex::state::shared
>
() {
s.lock();
u.unlock();
}
void upgradeable_mutex::lock() {
transition_up<state::unlocked, state::unique>();
}
void upgradeable_mutex::unlock() {
transition_down<state::unique, state::unlocked>();
}
void upgradeable_mutex::lock_shared() {
transition_up<state::unlocked, state::shared>();
}
void upgradeable_mutex::unlock_shared() {
transition_down<state::shared, state::unlocked>();
}
void upgradeable_mutex::lock_aspiring() {
transition_up<state::unlocked, state::aspiring>();
}
void upgradeable_mutex::unlock_aspiring() {
transition_down<state::aspiring, state::unlocked>();
}
void upgradeable_mutex::aspiring_to_unique() {
transition_up<state::aspiring, state::unique>();
}
void upgradeable_mutex::unique_to_aspiring() {
transition_down<state::unique, state::aspiring>();
}
void upgradeable_mutex::aspiring_to_shared() {
transition_down<state::aspiring, state::shared>();
}
void upgradeable_mutex::unique_to_shared() {
transition_down<state::unique, state::shared>();
}
I attempt to get the compiler to work out some of the above transitions "for me" with the transition_up and transition_down trick. I think I can do better, and it did increase code bulk significantly.
Having it 'auto-write' the unlocked-to-unique, and unique-to-(unlocked|shared) was all I got out of it. So probably not worth it.
Creating smart RAII objects that use the above is a bit tricky, as they have to support some transitions that the default unique_lock and shared_lock do not support.
You could just write aspiring_lock and then do conversions in there (either as operator unique_lock, or as methods that return said, etc), but the ability to convert from unique_lock&& down to shared_lock is exclusive to upgradeable_mutex and is a bit tricky to work with implicit conversions...
live example.

Here's my usual suggestion: Seqlock
You can have a single writer and many readers concurrently. Writers compete using a spinlock. A single writer doesn't need to compete so is cheaper.
Readers are truly only reading. They're not writing any state variables, counters, etc. This means you don't really know how many readers are there. But also, there no cache line ping pong so you get the best performance possible in terms of latency and throughput.
What's the catch? the data almost has to be POD. It doesn't really have to POD, but it can not be invalidated (no deleting std::map nodes) as readers may read it while it's being written.
It's only after the fact that readers discover the data is possibly bad and they have to re-read.
Yes, writers don't wait for readers so there's no concept of upgrade/downgrade. You can unlock one and lock the other. You pay less than with any sort of mutex but the data may have changed in the process.
I can go into more detail if you like.

The std::shared_mutex (as implemented in boost if not available on your platform(s)) provides some alternative for the problem.
For atomic upgrade lock semantics, the boost upgrade lock may be the best cross platform alternative.
It does not have an upgrade and downgrade locking mechanism you are looking for, but to get an exclusive lock, the shared access can be relinquished first, then exclusive access sought.
// assumes shared_lock with shared access has been obtained
ReadContainerLock container = state.getContainer();
auto value = container.find( "foo" )->bar;
{
container.shared_mutex().unlock();
// Upgrade read lock to write lock
std::unique_lock<std::shared_mutex> write(container.shared_mutex());
// container work...
write.unlock();
container.shared_mutex().lock_shared();
} // Downgrades write lock to read lock
A utility class can be used to cause the re-locking of the shared_mutex at the end of the scope;
struct re_locker {
re_locker(std::shared_mutex& m) : m_(m) { m_.unlock(); }
~re_locker() { m_.shared_lock(); }
// delete the copy and move constructors and assignment operator (redacted for simplicity)
};
// ...
auto value = container.find( "foo" )->bar;
{
re_locker re_lock(container.shared_mutex());
// Upgrade read lock to write lock
std::unique_lock<std::shared_mutex> write(container.shared_mutex());
// container work...
} // Downgrades write lock to read lock
Depending on what exception guarantees you want or require, you may need to add a "can re-lock" flag to the re_locker to either do the re-lock or not if an exception is thrown during the container operations/work.

Related

Is it ok to use atomics to reduce locking in a read dominant multithread program?

Recently, I found myself often in a situation that shared data get read a lot, but written rarely, so I begin to wonder is it possible to speed up the sync a little bit.
Take the following as an example, in which mutiple threads occasionally write the data, a single thread frequently read the data, all synched with a normal mutex.
#include <iostream>
#include <unistd.h>
#include <unordered_map>
#include <mutex>
#include <thread>
using namespace std;
unordered_map<int, int> someData({{1,10}});
mutex mu;
void writeData(){
while(true){
{
lock_guard<mutex> lock(mu);
int r = rand()%10;
someData[1] = r;
printf("data changed to %d\n", r);
}
usleep(rand()%100000000 + 100000000);
}
}
void readData(){
while(true){
{
lock_guard<mutex> lock(mu);
for(auto &i:someData){
printf("%d:%d\n", i.first, i.second);
}
}
usleep(100);
}
}
int main() {
thread writeT1(&writeData2);
thread writeT2(&writeData2);
thread readT(&readData2);
readT.join();
}
using normal lock mechanism, every read requires a lock, and I'm thinking to speed up to a single atomic read in most cases:
unordered_map<int, int> someData({{1,10}});
mutex mu;
atomic_int dataVersion{0};
void writeData2(){
while(true){
{
lock_guard<mutex> lock(mu);
dataVersion.fetch_add(1, memory_order_acquire);
int r = rand()%10;
someData[1] = r;
printf("data changed to %d\n", r);
}
usleep(rand()%100000000 + 100000000);
}
}
void readData2(){
mu.lock();
int versionCopy = dataVersion.load();
auto dataCopy = someData;
mu.unlock();
while(true){
if (versionCopy != dataVersion.load(memory_order_relaxed)){
lock_guard<mutex> lock(mu);
versionCopy = dataVersion.load(memory_order_relaxed);
dataCopy = someData;
}
else{
for(auto &i:dataCopy){
printf("%d:%d\n", i.first, i.second);
}
usleep(100);
}
}
}
The data type unordered_map here is just an example, it could be any type, and I'm not looking for a pure lock-free algorithm, as that might be a whole other story. Just for a normal lock based sync, in a situation that most operation is read, using a trick like this, is it logically ok? Are there any established approaches for this?
[edit]
I'm aware of the shared mutex, but it isn't really the situation that I was talking about. firstly shared lock is not cheap, probably more expensive than the plain mutex, certainly heavier than atomics; secondly, in the example I showed a single reading thread which can't take much advantage of it.
I was interested particularly in the locking operation cost. Reducing blocking, critical section sure is the first thing to look at in a real case, but I wasn't targeting that here.
The unordered_map data type is just an example, not looking for a data structure that better suits for a specific task, or a lock free algorithm, the data type could be anything.
sleep time is to demonstrate that read happens way much more than write, to a degree that we begin to not so care the extra lock and copy time in the if block.
Thanks~
You are storing the data in an unordered_map. What guarantees does the unordered_map class make about concurrent access for readers & writers. If it is unhappy with that prospect, the atomics are not your friend.
In most (every?) OS, locking primitives themselves are handled with atomics in the uncontested case; only reverting to a kernel when contested. With that in mind, you are best to minimize the amount of code while the lock is held, so your first loop should be:
int r = rand()%10;
mu.lock();
someData[1] = r;
mu.unlock();
printf("data changed to %d\n", r);
I don't know how you would fix the read side, but if you chose a friendlier data store, you could minimize access to it in the same way.
I will first try to describe my own understanding of your idea:
Frequent reads, occasional write.
Locks are expensive ... should be benchmarked, try std::shared_mutex or Slim Reader/Writer SRW Locks - Windows only, or some other slim implementation, which usually use some cheap and optimistic (atomic/spin-lock) mechanism, that has little-to-no impact in case of no collision (no writer most of the time).
You don't seem to care how old/recent your copy is. That is acceptable for some informative performance counters, but I would think twice about that - it is not something somebody else having to maintain your code would expect or even think about. The consequences can be catastrophic.
You only access the writable data under the lock, read a copy you create holding the lock. That means your approach is safe from simple threading synchronization view, except the above point (readers working with old data, multiple readers can have different copies ... is it worth it?).
Anyway, you should really try to benchmark first and then try to find better solution which somebody else already wrote (slim rw-locks), before even attemting to come-up with your own synchronization mechanism (that is generally very hard to do correctly).
EDIT: Found some article with concreate shared_mutex implementation using std::atomic:
Code Project: We make a std::shared_mutex 10 times faster
Coliru test here

Locking pairs of mutually referencing resources

Consider the following:
// There are guys:
class Guy {
// Each guy can have a buddy:
Guy* buddy; // When a guy has a buddy, he is his buddy's buddy, i.e:
// assert(!buddy || buddy->buddy == this);
public:
// When guys are birthed into the world they have no buddy:
Guy()
: buddy{}
{}
// But guys can befriend each other:
friend void befriend(Guy& a, Guy& b) {
// Except themselves:
assert(&a != &b);
// Their old buddies (if any), lose their buddies:
if (a.buddy) { a.buddy->buddy = {}; }
if (b.buddy) { b.buddy->buddy = {}; }
a.buddy = &b;
b.buddy = &a;
}
// When a guy moves around, he keeps track of his buddy
// and lets his buddy keep track of him:
friend void swap(Guy& a, Guy& b) {
std::swap(a.buddy, b.buddy);
if (a.buddy) { a.buddy->buddy = &a; }
if (b.buddy) { b.buddy->buddy = &b; }
}
Guy(Guy&& guy)
: Guy()
{
swap(*this, guy);
}
Guy& operator=(Guy guy) {
swap(*this, guy);
return *this;
}
// When a Guy dies, his buddy loses his buddy.
~Guy() {
if (buddy) { buddy->buddy = {}; }
}
};
All is well so far, but now I want this to work when buddies are used in different threads. No problem, let's just stick std::mutex in Guy:
class Guy {
std::mutex mutex;
// same as above...
};
Now I just have to lock mutexes of both guys before linking or unlinking the pair of them.
This is where I am stumped. Here are failed attempts (using the destructor as an example):
Deadlock:
~Guy() {
std::unique_lock<std::mutex> lock{mutex};
if (buddy) {
std::unique_lock<std::mutex> buddyLock{buddy->mutex};
buddy->buddy = {};
}
}
When two buddies are destroyed at around the same time, it is possible that each of them locks their own mutex, before trying to lock their buddies' mutexes, thus resulting in a deadlock.
Race condition:
Okay so we just have to lock mutexes in consistent order, either manually or with std::lock:
~Guy() {
std::unique_lock<std::mutex> lock{mutex, std::defer_lock};
if (buddy) {
std::unique_lock<std::mutex> buddyLock{buddy->mutex, std::defer_lock};
std::lock(lock, buddyLock);
buddy->buddy = {};
}
}
Unfortunately, to get to buddy's mutex we have to access buddy which at this point is not protected by any lock and may be in the process of being modified from another thread, which is a race condition.
Not scalable:
Correctness can be attained with a global mutex:
static std::mutex mutex;
~Guy() {
std::unique_lock<std::mutex> lock{mutex};
if (buddy) { buddy->buddy = {}; }
}
But this is undesirable for performance and scalability reasons.
So is this possible to do without global lock? How?
Using std::lock isn't a race condition (per se) nor does it risk deadlock.
std::lock will use a deadlock-free algorithm to obtain the two locks. They will be some kind of (unspecified) try-and-retreat method.
An alternative is to determine an arbitrary lock order for example using the physical address of the objects.
You've correctly excluded the possibility that an objects buddy is itself so there's no risk of trying to lock() the same mutex twice.
I say isn't a race condition per se because what that code will do is ensure integrity that if a has buddy b then b has buddy a for all a and b.
The fact that one moment after befriending two objects they might be unfriended by another thread is presumably what you intend or addressed by other synchronization.
Note also that when you're befriending and may unfriend the friends of the new friends you need to lock ALL the objects at once.
That is the two 'to be' friends and their current friends (if any).
Consequently you need to lock 2,3 or 4 mutexes.
std::lock unfortunately doesn't take an array but there is a version that does that in boost or you need to address it by hand.
To clarify, I'm reading the examples of possible destructors as models. Synchronization on the same locks would be required in all the relevant members (e.g. befriend(), swap() and unfriend() if that is required). Indeed the issue of locking 2,3 or 4 applies to the befriend() member.
Furthermore the destructor is probably the worst example because as mentioned in a comment it's illogical that an object is destructible but may be in lock contention with another thread. Some synchronization surely needs to exist in the wider program to make that impossible at which point the lock in the destructor is redundant.
Indeed a design which ensures Guy objects have no buddy before destruction would seem like a good idea and a debug pre-condition that checks assert(buddy==nullptr) in the destructor. Sadly that can't be left as a run-time exception because throwing exceptions in destructors can cause program termination (std::terminate()).
In fact the real challenge (which may depend on the surrounding program) is how to unfriend when befriending. That would appear to require a try-retreat loop:
Lock a and b.
Find out if they have buddies.
If they're already buddies - you're done.
If they have other buddies, unlock a & b, and lock a and b and their buddies (if any).
Check that the buddies haven't changed, if they have go again.
Adjust the relevant members.
It's a question for the surrounding program whether that risks live-lock but any try-retreat method suffers the same risk.
What won't work is std::lock() a & b then std::lock() the buddies because that does risk deadlock.
So to answer the question - yes, it is possible without a global lock but that depends on the surrounding program. It may be in a population of many Guy objects contention is rare and liveness is high.
But it may be that there are a small number of objects that are hotly contended (possibly in a large population) that results in a problem. That can't be assessed without understanding the wider application.
One way to resolve that would be lock escalation which in effect piecemeal falls back to a global lock. In essence that would mean if there were too many trips round the re-try loop a global semaphore would be set ordering all threads to go into global lock mode for a while. A while might be a number of actions or a period of time or until contention on the global lock subsides!
So the final answer is "yes it's absolutely possible unless it doesn't work in which case 'no'".

Upgrading read lock to write lock without releasing the first in C++11?

I know it's possible using boost::UpgradeLockable in C++14.
Is there anything similar for C++11?
An upgradeable lock can be written on top of simpler locking primitives.
struct upgradeable_timed_mutex {
void lock() {
upgradable_lock();
upgrade_lock();
}
void unlock() {
upgrade_unlock();
upgradable_unlock();
}
void shared_lock() { shared.shared_lock(); }
void shared_unlock() { shared.shared_unlock(); }
void upgradable_lock() { unshared.lock(); }
void ungradable_unlock() { unshared.unlock(); }
void upgrade_lock() { shared.lock(); }
void upgrade_unlock() { shared.unlock(); }
private:
friend struct upgradable_lock;
std::shared_timed_mutex shared;
std::timed_mutex unshared;
};
and similar for the timed and try variants. Note that timed variants which access two mutexes in a row have to do some extra work to avoid spending up to 2x the requested time, and try_lock has to be careful about the first lock's state in case the 2nd fails.
Then you have to write upgradable_lock, with the ability to spawn a std::unique_lock upon request.
Naturally this is hand-written thread safety code, so it is unlikely to be correct.
In C++1z you can write an untimed version as well (with std::shared_mutex and std::mutex).
Less concretely, there can be exactly one upgradeable or write lock at a time. This is what the unshared mutex represents.
So long as you hold unshared, nobody else is writing to the guarded data, so you can read from it without holding the shared mutex at all.
When you want to upgrade, you can grab a unique lock on the shared mutex. This cannot deadlock so long as no readers try to upgrade to upgradable. This excludes readers from reading, you can write, and then you release it and return back to a read only state (where you only hold the unshared mutex).

Thread safe container

There is some exemplary class of container in pseudo code:
class Container
{
public:
Container(){}
~Container(){}
void add(data new)
{
// addition of data
}
data get(size_t which)
{
// returning some data
}
void remove(size_t which)
{
// delete specified object
}
private:
data d;
};
How this container can be made thread safe? I heard about mutexes - where these mutexes should be placed? Should mutex be static for a class or maybe in global scope? What is good library for this task in C++?
First of all mutexes should not be static for a class as long as you going to use more than one instance. There is many cases where you should or shouldn't use use them. So without seeing your code it's hard to say. Just remember, they are used to synchronise access to shared data. So it's wise to place them inside methods that modify or rely on object's state. In your case I would use one mutex to protect whole object and lock all three methods. Like:
class Container
{
public:
Container(){}
~Container(){}
void add(data new)
{
lock_guard<Mutex> lock(mutex);
// addition of data
}
data get(size_t which)
{
lock_guard<Mutex> lock(mutex);
// getting copy of value
// return that value
}
void remove(size_t which)
{
lock_guard<Mutex> lock(mutex);
// delete specified object
}
private:
data d;
Mutex mutex;
};
Intel Thread Building Blocks (TBB) provides a bunch of thread-safe container implementations for C++. It has been open sourced, you can download it from: http://threadingbuildingblocks.org/ver.php?fid=174 .
First: sharing mutable state between threads is hard. You should be using a library that has been audited and debugged.
Now that it is said, there are two different functional issue:
you want a container to provide safe atomic operations
you want a container to provide safe multiple operations
The idea of multiple operations is that multiple accesses to the same container must be executed successively, under the control of a single entity. They require the caller to "hold" the mutex for the duration of the transaction so that only it changes the state.
1. Atomic operations
This one appears simple:
add a mutex to the object
at the start of each method grab a mutex with a RAII lock
Unfortunately it's also plain wrong.
The issue is re-entrancy. It is likely that some methods will call other methods on the same object. If those once again attempt to grab the mutex, you get a dead lock.
It is possible to use re-entrant mutexes. They are a bit slower, but allow the same thread to lock a given mutex as much as it wants. The number of unlocks should match the number of locks, so once again, RAII.
Another approach is to use dispatching methods:
class C {
public:
void foo() { Lock lock(_mutex); foo_impl(); }]
private:
void foo_impl() { /* do something */; foo_impl(); }
};
The public methods are simple forwarders to private work-methods and simply lock. Then one just have to ensure that private methods never take the mutex...
Of course there are risks of accidentally calling a locking method from a work-method, in which case you deadlock. Read on to avoid this ;)
2. Multiple operations
The only way to achieve this is to have the caller hold the mutex.
The general method is simple:
add a mutex to the container
provide a handle on this method
cross your fingers that the caller will never forget to hold the mutex while accessing the class
I personally prefer a much saner approach.
First, I create a "bundle of data", which simply represents the class data (+ a mutex), and then I provide a Proxy, in charge of grabbing the mutex. The data is locked so that the proxy only may access the state.
class ContainerData {
protected:
friend class ContainerProxy;
Mutex _mutex;
void foo();
void bar();
private:
// some data
};
class ContainerProxy {
public:
ContainerProxy(ContainerData& data): _data(data), _lock(data._mutex) {}
void foo() { data.foo(); }
void bar() { foo(); data.bar(); }
};
Note that it is perfectly safe for the Proxy to call its own methods. The mutex will be released automatically by the destructor.
The mutex can still be reentrant if multiple Proxies are desired. But really, when multiple proxies are involved, it generally turns into a mess. In debug mode, it's also possible to add a "check" that the mutex is not already held by this thread (and assert if it is).
3. Reminder
Using locks is error-prone. Deadlocks are a common cause of error and occur as soon as you have two mutexes (or one and re-entrancy). When possible, prefer using higher level alternatives.
Add mutex as an instance variable of class. Initialize it in constructor, and lock it at the very begining of every method, including destructor, and unlock at the end of method. Adding global mutex for all instances of class (static member or just in gloabl scope) may be a performance penalty.
The is also a very nice collection of lock-free containers (including maps) by Max Khiszinsky
LibCDS1 Concurrent Data Structures
Here is the documentation page:
http://libcds.sourceforge.net/doc/index.html
It can be kind of intimidating to get started, because it is fully generic and requires you register a chosen garbage collection strategy and initialize that. Of course, the threading library is configurable and you need to initialize that as well :)
See the following links for some getting started info:
initialization of CDS and the threading manager
http://sourceforge.net/projects/libcds/forums/forum/1034512/topic/4600301/
the unit tests ((cd build && ./build.sh ----debug-test for debug build)
Here is base template for 'main':
#include <cds/threading/model.h> // threading manager
#include <cds/gc/hzp/hzp.h> // Hazard Pointer GC
int main()
{
// Initialize \p CDS library
cds::Initialize();
// Initialize Garbage collector(s) that you use
cds::gc::hzp::GarbageCollector::Construct();
// Attach main thread
// Note: it is needed if main thread can access to libcds containers
cds::threading::Manager::attachThread();
// Do some useful work
...
// Finish main thread - detaches internal control structures
cds::threading::Manager::detachThread();
// Terminate GCs
cds::gc::hzp::GarbageCollector::Destruct();
// Terminate \p CDS library
cds::Terminate();
}
Don't forget to attach any additional threads you are using:
#include <cds/threading/model.h>
int myThreadFunc(void *)
{
// initialize libcds thread control structures
cds::threading::Manager::attachThread();
// Now, you can work with GCs and libcds containers
....
// Finish working thread
cds::threading::Manager::detachThread();
}
1 (not to be confuse with Google's compact datastructures library)

Multiple-readers, single-writer locks in Boost

I'm trying to implement the following code in a multithreading scenario:
Get shared access to mutex
Read data structure
If necessary:
Get exclusive access to mutex
Update data structure
Release exclusive lock
Release shared lock
Boost threads has a shared_mutex class which was designed for a multiple-readers, single-writer model. There are several stackoverflow questions regarding this class. However, I'm not sure it fits the scenario above where any reader may become a writer. The documentation states:
The UpgradeLockable concept is a
refinement of the SharedLockable
concept that allows for upgradable
ownership as well as shared ownership
and exclusive ownership. This is an
extension to the multiple-reader /
single-write model provided by the
SharedLockable concept: a single
thread may have upgradable ownership
at the same time as others have shared
ownership.
From the word "single" I suspect that only one thread may hold an upgradable lock. The others only hold a shared lock which can't be upgraded to an exclusive lock.
Do you know if boost::shared_lock is useful in this situation (any reader may become a writer), or if there's any other way to achieve this?
Yes, you can do exactly what you want as shown in the accepted answer here. A call to upgrade to exclusive access will block until all readers are done.
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
}
boost::shared_lock doesn't help in this situation (multiple readers that can become writers), since only a single thread may own an upgradable lock. This is both implied by the quote from the documentation in the question, and by looking at the code (thread\win32\shared_mutex.hpp). If a thread tries to acquire an upgradable lock while another thread holds one, it will wait for the other thread.
I settled on using a regular lock for all reader/writers, which is OK in my case since the critical section is short.
You know LightweightLock
or same at LightweightLock_zip
does exactly what you want.
I have used it a long time.
[EDIT]
here's the source:
/////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 1995-2002 Brad Wilson
//
// This material is provided "as is", with absolutely no warranty
// expressed or implied. Any use is at your own risk. Permission to
// use or copy this software for any purpose is hereby granted without
// fee, provided the above notices are retained on all copies.
// Permission to modify the code and to distribute modified code is
// granted, provided the above notices are retained, and a notice that
// the code was modified is included with the above copyright notice.
//
/////////////////////////////////////////////////////////////////////////////
//
// This lightweight lock class was adapted from samples and ideas that
// were put across the ATL mailing list. It is a non-starving, kernel-
// free lock that does not order writer requests. It is optimized for
// use with resources that can take multiple simultaneous reads,
// particularly when writing is only an occasional task.
//
// Multiple readers may acquire the lock without any interference with
// one another. As soon as a writer requests the lock, additional
// readers will spin. When the pre-writer readers have all given up
// control of the lock, the writer will obtain it. After the writer
// has rescinded control, the additional readers will gain access
// to the locked resource.
//
// This class is very lightweight. It does not use any kernel objects.
// It is designed for rapid access to resources without requiring
// code to undergo process and ring changes. Because the "spin"
// method for this lock is "Sleep(0)", it is a good idea to keep
// the lock only long enough for short operations; otherwise, CPU
// will be wasted spinning for the lock. You can change the spin
// mechanism by #define'ing __LW_LOCK_SPIN before including this
// header file.
//
// VERY VERY IMPORTANT: If you have a lock open with read access and
// attempt to get write access as well, you will deadlock! Always
// rescind your read access before requesting write access (and,
// of course, don't rely on any read information across this).
//
// This lock works in a single process only. It cannot be used, as is,
// for cross-process synchronization. To do that, you should convert
// this lock to using a semaphore and mutex, or use shared memory to
// avoid kernel objects.
//
// POTENTIAL FUTURE UPGRADES:
//
// You may consider writing a completely different "debug" version of
// this class that sacrifices performance for safety, by catching
// potential deadlock situations, potential "unlock from the wrong
// thread" situations, etc. Also, of course, it's virtually mandatory
// that you should consider testing on an SMP box.
//
///////////////////////////////////////////////////////////////////////////
#pragma once
#ifndef _INC_CRTDBG
#include
#endif
#ifndef _WINDOWS_
#include
#endif
#ifndef __LW_LOCK_SPIN
#define __LW_LOCK_SPIN Sleep(0)
#endif
class LightweightLock
{
// Interface
public:
// Constructor
LightweightLock()
{
m_ReaderCount = 0;
m_WriterCount = 0;
}
// Destructor
~LightweightLock()
{
_ASSERTE( m_ReaderCount == 0 );
_ASSERTE( m_WriterCount == 0 );
}
// Reader lock acquisition and release
void LockForReading()
{
while( 1 )
{
// If there's a writer already, spin without unnecessarily
// interlocking the CPUs
if( m_WriterCount != 0 )
{
__LW_LOCK_SPIN;
continue;
}
// Add to the readers list
InterlockedIncrement((long*) &m_ReaderCount );
// Check for writers again (we may have been pre-empted). If
// there are no writers writing or waiting, then we're done.
if( m_WriterCount == 0 )
break;
// Remove from the readers list, spin, try again
InterlockedDecrement((long*) &m_ReaderCount );
__LW_LOCK_SPIN;
}
}
void UnlockForReading()
{
InterlockedDecrement((long*) &m_ReaderCount );
}
// Writer lock acquisition and release
void LockForWriting()
{
// See if we can become the writer (expensive, because it inter-
// locks the CPUs, so writing should be an infrequent process)
while( InterlockedExchange((long*) &m_WriterCount, 1 ) == 1 )
{
__LW_LOCK_SPIN;
}
// Now we're the writer, but there may be outstanding readers.
// Spin until there aren't any more; new readers will wait now
// that we're the writer.
while( m_ReaderCount != 0 )
{
__LW_LOCK_SPIN;
}
}
void UnlockForWriting()
{
m_WriterCount = 0;
}
long GetReaderCount() { return m_ReaderCount; };
long GetWriterConut() { return m_WriterCount; };
// Implementation
private:
long volatile m_ReaderCount;
long volatile m_WriterCount;
};