0

I have a class (class A) that is using the monitor pattern for safe access to a std::deque. Any time I need to simply add or remove an element I am fine. But sometimes a different class (class B) needs to call in and get a reference to an element in the deque in class A.

I have been returning an iterator to that element, but there's the chance that another thread could 'push' a new element into class A and invalidate the iterator that the class B is using. The only way I can think to prevent this is to lock a mutex in class A before returning the iterator, and then have the class B call another function when it's finished to release the mutex, but this seems hacky.

Any ideas for a cleaner way?

quamrana
  • 37,849
  • 12
  • 53
  • 71
Steve
  • 25
  • 3
  • Shared mutable state between threads is often a source of hard to fix problems. I try to avoid that as much as possible. However, it is an architectural/design problem and changing it e.g. to a task- or message-based system may be out of scope for you. Howver, I would recommend to watch https://channel9.msdn.com/Events/GoingNative/2013/Cpp-Seasoning, especially the part about "No raw snychronization primitives" from slide 41. – Jens Aug 29 '15 at 09:43
  • Push doesn't invalidate iterators into a queue – MikeMB Aug 29 '15 at 10:08
  • @MikeMB Your comment forced me to re-read how push_back() works. It actually does invalidate the iterators, but pointers and references are OK. That could be helpful in my situation. (I thought 'everything' was invalidated.) – Steve Aug 30 '15 at 02:24
  • @Steve: Sorry for the confusion, I should have checked the documentation (haven't used deque in a while) – MikeMB Aug 30 '15 at 08:35

2 Answers2

2

No matter how you approach it, it boils down to using a std::mutex to implement thread-safe access to objects and class members.

You could use a std::mutex just long enough to make a copy of the objects or class members, and return the copy to the caller, to do with it as it wishes.

But if you do not wish to make private thread copies of shared class members or objects, this is pretty much the only way to do it. This is fairly fundamental, and all kinds of high-level thread-safe libraries all end up doing this, on the fundamental level.

With C++11, there is some syntactic sugar that can be implemented, using lambdas. Let's take a simple case of implementing a thread-safe int:

class safe_int {

    int n;

    std::mutex m;

public:

    template<typename functor_type>
    void lock(functor_type &&functor)
    {
          std::unique_lock<std::mutex> lock(m);

          functor(n);
    }
};

Then, to access the int in a thread-safe manner:

void do_something_with(safe_int *i)
{
     i->lock([]
             (int &n)
             {
                // Do something with 'n'. Use it. Set its value, whatever.
             });
}

The helper class method acquires a lock on the mutex, and invokes the lambda with the locked mutex, passing a reference to the private, mutex-protected, class member to the lambda. When the lambda returns, the lock gets automatically released.

Going forward, it is no longer necessary to explicit lock and unlock the mutex, just invoke lock(). Thread safety is enforced by contract.

Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148
  • Thanks! That seems like a nice clean solution. I use lambdas internally to the class that contains the deque, but just didn't think of passing one in. I'm accepting your answer as the most helpful. – Steve Aug 30 '15 at 02:38
0

I think your question is way too vague to have a definitive answer. For example, one answer could be: just store shared_ptrs in your deque. But then you might argue "it's not good for me because there's an additional allocation/indirection" or it may be good enough for you. There are a lot of questions about your use case that need to be clarified before really answering your question. Such as:

  • Can the objects be mutated or your deque contains only constant values, or, say A is the only one that can modify the elements of the deque, while B can only read from them (this leads to using different locks, put into different places).
  • Why exactly are you using a deque? Can you use another container? (See for example this answer for a quote from the Standard about iterators of associative containers).
  • What is/are the element type(s) stored in your deque(s)?
  • What are the expected size of your data and required performance for this code?

etc...

A concise question is often nice, but in this case more context would be helpful.

Community
  • 1
  • 1
Rostislav
  • 3,857
  • 18
  • 30
  • I use a deque since one thread constantly pushes new elements onto the back, and another thread is constantly feeding from the front. From what I read, a deque was most efficient at these two operations. Although occasionally an item is removed from somewhere in between. Finally I dug into 'shared_ptr' a bit and that surely will be helpful at some point. Thanks. – Steve Aug 30 '15 at 02:50