5
struct Data
{
 ...
 CRITICAL_SECTION valLock;
}    
std::map<int, Data> mp;
CRITICAL_SECTION mpLock;

I am currently using two critical sections to make this thread safe.

I have to lock both map and Data for updating a Data

//Lock mpLock
//Lock mp[key1].valLock
mp[key1].something = something_new;
//unlock mp[key1].valLock
//unlock mpLock

I looked into intel's concurrent hashmap, which does not need two locks and handles this internally.Is there any other way, if i don't want to use intel's tbb. I have only c++ 98 support.Can use boost though. Looked into boost::shared_mutex, but could not relate how i can use it in my current scenario.

EDIT: Is the lock on container really required? Can't i use Data::valLock to read/write Data.Any insertion in mp is not going to affect the existing iterators, so no lock is needed.Any deletion from mp will be preceded with Data::valLock.What cases could be missed here?

EDIT 2:

UpdateThread()
{
   //Lock mp[key].valLock
   mp[key].a = b;         //Line 1
   //unlock mp[key].valLock
}

ReadThread()
{
    //Lock mp[key].valLock
   something = mp[key].a;   //Line 2
   //unlock mp[key].valLock
}

So i am thinking that Line 2 can execute only if Line 1 has completed(or vice versa) i.e. mp has been updated(along with the map internals).So is it not safe? If not then it means if one thread modifies mp[key1], and another reads mp[key2], this is data race?

user3819404
  • 611
  • 6
  • 18

2 Answers2

4

One mutex is needed to make the container thread-safe. And a per-object mutex to make each of these objects thread-safe.

A per-object mutex is rather a sub-optimal design. Another design is use copyable but immutable objects or store shared/intrusive pointers in the container.

With immutable objects readers lock the container (for read), make a copy of the element and unlock the container. Writers lock the container (for write) add/remove/modify elements and unlock. Because readers always make a copy of the element, there is never a contention between threads on elements.

With shared pointers readers do as above. Writers also do as above, but rather then modifying existing elements, writers always create a new element and replace the existing one.

Example with immutable objects:

template<class Key, class Value>
class ThreadSafeMap
{
    std::mutex m_;
    std::map<Key, Value> c_;

public:
    Value get(Key const& k) {
        std::unique_lock<decltype(m_)> lock(m_);
        return c_[k]; // Return a copy.
    }

    template<class Value2>
    void set(Key const& k, Value2&& v) {
        std::unique_lock<decltype(m_)> lock(m_);
        c_[k] = std::forward<Value2>(v);
    }
};

You may also like to use std::unordered_map (or open source ones) instead of std::map to get better performance. std::map is rather cache-unfriendly.

Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271
  • Thanks, Can you respond to my edited question? – user3819404 Feb 26 '18 at 12:09
  • Also, going by your approach, while updating a single element, all elements in map will be locked.Right? – user3819404 Feb 26 '18 at 12:11
  • @user3819404 It locks the container while both reading and writing an element. Modifying an element is read followed by write. In `get` you can use `shared_lock`, in `set` - `unique_lock`. – Maxim Egorushkin Feb 26 '18 at 12:17
  • @user3819404 The lock on the container is required if there is at least one writer. If all users of the container are readers no lock is required. – Maxim Egorushkin Feb 26 '18 at 12:22
  • The writer will try to acquire `Data::valLock` before writing and any other reader if exists would also be using this lock.So what is the need to lock the entire container?I understand that now it does not allow multiple readers, single writer, but this should work.Please correct me. – user3819404 Feb 26 '18 at 12:29
  • @user3819404 You need to understand that all standard container are not thread-safe. If one thread adds/removes an element while another thread is reading the container - the reader thread is going to read corrupted data or crash reading it. If two threads modify a container simultaneously the container is going to get corrupted. You need to lock the entire container if there are more than one thread accessing the container and at least one thread is a writer. With a shared mutex readers lock it for shared access, whereas writers lock it for exclusive access. – Maxim Egorushkin Feb 26 '18 at 12:35
  • I'm not getting the "immutable" bit. You mutate elements right there in `set`. Mandating copies on observation certainly alleviates a bit of contention though, and makes the whole thing less error prone at the callsite. – Lightness Races in Orbit Feb 26 '18 at 12:42
  • I thought if two threads access same element in map, then only it is a data race(which can be prevented using `Data:valLock`).If threads access different elements of map, it is safe. – user3819404 Feb 26 '18 at 12:43
  • @user3819404: The container itself has logic and algorithms that make it work. Those are not inherently thread-safe. If you have two threads doing things with the container's internals at the same time, that's bad. You have to _get to_ your element before you even do anything with its value. It is true that only _accessing_ an element (not creating one) may work in practice, as nothing is modified, but you don't know enough about the internals of the container type to guarantee that (and remember that `c_[k]` creates an element if it doesn't exist already!). A lock is needed for thread-safety. – Lightness Races in Orbit Feb 26 '18 at 12:44
  • @LightnessRacesinOrbit But here it is mentioned, https://stackoverflow.com/a/15067564/3819404 – user3819404 Feb 26 '18 at 12:46
  • @user3819404: Yes but **1.** `operator[]` is not a `const` method (though you could fix this by switching to `find()`, and **2.** what if another thread is writing while you want to read? That answer does explain this. – Lightness Races in Orbit Feb 26 '18 at 12:46
  • @LightnessRacesinOrbit but `c_[k]` would have acquired lock before doing that operation.If any other tries to read `c_[k]`, it will be blocked. – user3819404 Feb 26 '18 at 12:48
  • @user3819404: You're still not considering the need for _the container itself_ to be thread-safed. While the map is being written to by some thread, the map is rewriting its internal tree, all sorts going on; `c_[k]` is not a safe thing to do while that is happening. You're only considering the data that has been retrieved from the container (which, as Maxim has shown, really doesn't benefit from its own lock). – Lightness Races in Orbit Feb 26 '18 at 12:48
  • @LightnessRacesinOrbit "immutable" means you do not get a reference to an object and then mutate it. Rather it always overwrites the entire object while locking the container's mutex. This eliminates the need for per-object mutex. – Maxim Egorushkin Feb 26 '18 at 12:50
  • I think immutable is a confusing term for that as long as you're providing a `set()` interface, but I get the intent. Strictly controlled mutability ;) I'd just say no direct access is permitted but that's not a pithy adjective so meh – Lightness Races in Orbit Feb 26 '18 at 12:51
  • @LightnessRacesinOrbit Somewhat imprecise and confusing, I agree. – Maxim Egorushkin Feb 26 '18 at 12:52
  • @LightnessRacesinOrbit Sorry if this seems stupid, but i am finding it hard to understand the need of lock on entire container.I have edited my question to reflect this. – user3819404 Feb 26 '18 at 13:03
  • 1
    @user3819404: What you're proposing is like getting into your car and driving off in it, while it's in pieces at the garage having a new engine fitted. I don't think I can explain it in any other way. Hopefully someone else can do a better job! – Lightness Races in Orbit Feb 26 '18 at 14:41
1

Is the lock on container really required?

Yes

Can't i use Data::valLock to read/write Data.

Of course you can, that's already what you're using it for.

Any insertion in mp is not going to affect the existing iterators, so no lock is needed

Wrong.

Insertions into a map don't invalidate existing iterators, but if you have two threads inserting or deleting at the same time (or one thread inserting/deleting and another thread finding), it's not safe. This is not because of iterators being invalidated, but because updating and traversing the internal node pointer graph can't safely be done concurrently.

The same is potentially true of any other node-based container.

Now, if you get (and keep) an iterator to your map while holding the container lock, you can keep referring to that single node via that iterator without holding the container lock, so long as the iterator isn't invalidated. This is fine.

If you want to do anything with the iterator other than dereference it, then you need the container lock again. Advancing the iterator, or finding another iterator, both have to traverse the node graph which (if you don't hold the lock) could be mutated underneath you by another thread performing an insertion or deletion.

Useless
  • 64,155
  • 6
  • 88
  • 132