12

Let me start by saying that I have read most SO and other topics on the subject.

The way I understand things, std::vector will reallocate memory when pushing back new items, which is my case, unless I have reserved enough space (which is not my case).

What I have is a vector of std::shared_ptr, and that vector holds unique objects (or more correctly, pointers to unique objects in the vector).

The handling of those objects via pointers is wrapped around a Factory & Handler class, but pointers to the objects are accessible from outside the wrapper class and can have member values modified. There is no deleting happening at any time.

If I am understanding correctly issues raised in previous SO questions about std::vector and thread safety, adding (push_back) new objects may invalidate previous pointers, as the vector internally may reallocate memory and copy everything over, which would of course be a disaster for me.

My intentions are to read from that vector, often modifying objects through the pointers, and add new items to the vector, from threads running asynchronously.

So,

  1. Using atomic or mutexes is not enough? If I push back from one thread, another thread handling an object via pointer may end up having an invalid object?
  2. Is there a library that can handle this form of MT issues? The one I keep reading about is Intel's TBB, but since I'm already using C++11, I'd love to keep changes to a minimum, even if it means more work on my part - I want to learn in the process, not just copy-paste.
  3. Other than locking access while modifying objects, I would want asynchronous parallel read access to the vector that will not be invalidated by push_backs. How can I achieve that?

If it is of any importance, all the above is on linux (debian jessie) using gcc-4.8 with c++11 enabled.

I am open to using minimally invasive libraries.

Many thanks in advance :-)

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
Ælex
  • 14,432
  • 20
  • 88
  • 129
  • 2
    Why don't you use an `std::deque` instead? It has similar performance guarantees, but doesn't reallocate when you `push_back` (OTOH, you still have to protect the `push_back` with a mutex, since you are modifying the internal state of the container). – Matteo Italia May 12 '14 at 23:30
  • I wouldn't mind swapping for a deck, provided its interchangable with vector without serious modification. Is std::deque guaranteed to be contiguous upon pushing back, thus resolving this issue? – Ælex May 12 '14 at 23:32
  • As @MatteoItalia mentioned queues are much better suitable to protect operations by thread safe producer/consumer semantics. They're not thread safe natively, but you can easily construct what you need, with a mutex or semaphore, or both. – πάντα ῥεῖ May 12 '14 at 23:33
  • The interface is substantially the same (O(1) random-index access, random access iterators, push/pop at both ends, ...), with the potential benefit that it's equally fast to push at the begin or the end of the container. The storage of course is not all contiguous (typically it's made of contiguous chunks), but this permits it to avoid reallocations when you push elements. – Matteo Italia May 12 '14 at 23:35
  • @MatteoItalia We've also made such things working based on `std::array` => _'Fixed Size Queue'_. The idioms still worked well, and thread safety/consumer producer patterns were placed a level above. – πάντα ῥεῖ May 12 '14 at 23:38
  • Well, thank you very very match Matteo, this would be an ideal solution for me. Out of curiosity, the alternative would be to use thread local variables, and then lock swapping operations to the vector objects (which seems increasingly more complex)? If you'd like to write an answer, I'll accept it :) – Ælex May 12 '14 at 23:40
  • @MatteoItalia To be honest, we didn't use `std::array` but good old _ring buffers_ underlying there (there wasn't really need for `std::array`) ;) ... – πάντα ῥεῖ May 13 '14 at 01:06
  • 2
    The values inside the container are guaranteed to be kept, there is no point in loosing your data when you add new values!. What is NOT guaranteed to be kept are the iterators to the container. e.g. if you started scanning the container from begin(v) to end(v) and on the same time, in another thread, you v.push_back(...) - then the scan may fail as the iterators may not be valid anymore. – Ran Regev May 13 '14 at 06:09

3 Answers3

7

adding (push_back) new objects may invalidate previous pointers ...

No, this operation doesn't invalidate any previous pointers, unless you are refering to addresses inside the vectors internal data management (which clearly isn't your scenario).
If you store raw pointers, or std::shared_ptr's there, those will be simply copied, and not get invalid.


As mentioned in comments a std::vector isn't very suitable to guarantee thread safety for producer / consumer patterns for a number of reasons. Neither storing raw pointers to reference the alive instances is!

A Queue will be much better to support this. As for standards you can use the std::deque to have ceratain access points (front(),back()) for the producer / consumer.

To make these access point's thread safe (for pushing/popping values) you can easily wrap them with your own class and use a mutex along, to secure insertion/deletion operations on the shared queue reference.
The other (and major, as from your question) point is: manage ownership and lifetime of the contained/referenced instances. You may also transfer ownership to the consumer, if that's suitable for your use case (thus getting off from the overhead with e.g. std::unique_ptr), see below ...

Additionally you may have a semaphore (condition variable), to notify the consumer thread, that new data is available.


'1. Using atomic or mutexes is not enough? If I push back from one thread, another thread handling an object via pointer may end up having an invalid object?'

The lifetime (and thus thread safe use) of the instances stored to the queue (shared container) need's to be managed separately (e.g. using smart pointers like std::shared_ptr or std::unique_ptr stored there).

'2. Is there a library ...'

It can be achieved all well with the existing standard library mechanisms IMHO.

As for point 3. see what's written above. As what I can tell further about this, it sounds like you're asking for something like a rw_lock mutex. You may provide a surrogate for this with a suitable condition variable.

Feel free to ask for more clarification ...

πάντα ῥεῖ
  • 1
  • 13
  • 116
  • 190
  • Hi, thanks for the answer and your help. Regarding the lifetime I've no concerns, shared_ptr is taking care of that. What I do have concerns, is that upon insertion, previously held pointers may become invalid. My first idea was to copy locally per thread an instance that is being read or modified. However, those instances have pointers to other instances (a Markov Decision Path) and therefore this would make this approach very complex. So, as far as I understand from your question, by using a std::deque, I do not have to worry for the validity of the pointer instances, provided I use locks? – Ælex May 13 '14 at 01:24
  • If you already have `shared_ptr` intances there you won't need to worry IMHO. Any resizing operations will take care of copying the contained values, and copying/moving of `shared_ptr` is safe (no need to worry about needing deep copies as well, there's still everything included). Same applies to `std::unique_ptr` as I mentioned, but depends if your instances should be really unique (-ly) produced by the producer, and are unique (-ly) consumed by the consumer. – πάντα ῥεῖ May 13 '14 at 01:32
  • We have scenarios, where we for e.g. use the good old `std::auto_ptr` to transfer ownership in the inteface, but use raw pointers in the queue (since `std::auto_ptr` isn't a _nice_ class). Though you needed to manipulate the `std::auto_ptr` at the surface of the _push/pop_ interface. The modern smart pointers like `std::shared_ptr` handle all this intrinsically and well reliable. – πάντα ῥεῖ May 13 '14 at 01:38
  • @Alex Sorry missed to address you. I've put some explanations and updated my answer a bit. Hope that clarifies about your misconceptions. – πάντα ῥεῖ May 13 '14 at 01:46
  • @KonradRudolph I didn't say that it does. The `std::deque` is simply better suited for implementation of producer/consumer patterns. I said it's necessary to protect the insert/delete operations with a mutex or alike. – πάντα ῥεῖ May 13 '14 at 10:01
  • Thank you for taking the time to clarify everything :-) – Ælex May 13 '14 at 17:20
3

Re-reading the question, the situation seems a bit different.

std::vector is ill-suited for storing objects to which you'd have to keep references, since a push_back can invalidate all the references to the stored objects. But, you are storing a bunch of std::shared_ptr.

The std::shared_ptrs stored inside should handle the resizing gracefully (they are being moved, but not the objects they point to) as long as, in the threads, you don't keep references to std::shared_ptrs stored inside the vector, but you keep copies of them.

Both using std::vector and std::deque you have to synchronize the access to the data structure, since a push_back, although not reference-invalidating, alters the internal structures of the deque, and thus is not allowed to run simultaneously with a deque access.

OTOH, std::deque is probably better suited anyway for performance reasons; at each resize you are moving around a lot of std::shared_ptr, which may have to do a locked increment/decrement to the refcount in case of copy/delete (if they are moved - as they should - this should be elided - but YMMV).

But most importantly, if your usage of std::shared_ptr is just to avoid the potential moves in the vector, you may drop it completely when using a deque, since the references are not invalidated, so you can store your objects directly in the deque instead of using heap allocation and the indirection/overhead of std::shared_ptr.

Community
  • 1
  • 1
Matteo Italia
  • 123,740
  • 17
  • 206
  • 299
  • Why would a vector _not_ use the move constructor of `std::shared_ptr` while _resizing_? I can see copying from a vector with a wierd allocator causing problems, but _resizing_? – Mooing Duck May 13 '14 at 00:30
  • The important part is the distinction you made (what I've been worried about). As far as I know, vectors will upon insertion, resize and then re-allocate memory. All my pointers to objects come from that vector, as the factory class that wraps around it, provides them to the threads. So, in the event that one thread is using one of those pointers, and another inserts a new one (and thus the vector reallocates), what happens to the object being accessed from the first thread? I have seen similar problems (not thread related) when inserting objects in a vector while iterating it. – Ælex May 13 '14 at 01:18
  • If the above comment isn't clear enough, my worry is what happens if I try to access through a pointer acquired earlier (thread t1), when another thread t2, has inserted a new object, and thus the vector has rearranged its pointers. My understanding is that the pointer from t1 is now invalid and could segfault (or undefined behaviour) as I am now trying to access an object through a pointer that is no longer valid. I imagine that using a (thread local) pointer to the object would solve the problem, but from your answer, queues are not going to rearrange the pointers ? – Ælex May 13 '14 at 01:28
  • 1
    @Alex _'my worry is what happens ...'_ See the clarifications on my answer. Hope this solves your woes ... – πάντα ῥεῖ May 13 '14 at 02:14
  • 2
    @Alex: you should not keep a pointer to an element in the vector, but a copy of it - i.e. an `std::shared_ptr` to the actual object you are interested in. Even if the "original" `std::shared_ptr` is moved around due to vector reallocations, each local copy will stay still, and will keep pointing to the same object in the heap (which is completely unaffected by the whole thing). – Matteo Italia May 13 '14 at 05:49
  • 1
    @MooingDuck: I don't know, *relata refero* (see John Dibling's comment in the linked answer, where he notices that copy and supposed-move times are identical). – Matteo Italia May 13 '14 at 05:51
3

If you will be always just adding new items to the container and then accessing them, what you could find useful is a vector with another indirection, so that instead of swaping the internal buffer for a bigger one, the once allocated space is never freed and a new space is just appended somehow in a thread-safe manner.

For example, it can look like this:

concurrent_vector<Object*>:
  size_t m_baseSize = 1000
  size_t m_size     = 3500
  part*  m_parts[6] = {
    part* part1, ----> Object*[1000]
    part* part2, ----> Object*[2000]
    part* part3, ----> Object*[4000]
    NULL,
    ...
  }

The class contains a fixed array of pointers to individual chunks of memory with the items, with exponentially increasing size. Here the limit is 6 parts, so 63000 items - but this can be changed easily.

The container starts with all parts pointers set to NULL. If an item is added, the 1st chunk is created, with size m_baseSize, here 1000, and saved to m_parts[0]. Subsequent items are written there.

When the chunk is full, another buffer is allocated, with twice the size of the previous one (2000), and stored into m_parts[1]. This is repeated as required.

All this can be done using atomic operations, but that is of course tricky. It can be simpler if all writers can be protected by a mutex and only readers are fully concurrent (e.g. if writing is much more rare operation). All reader threads always see either NULL in m_parts[i] or NULL in one of the buffers, or a valid pointer. Existing items are never moved in memory, invalidated or anything.


As far as existing libraries go, you might want to look at Thread Building Blocks by Intel, particularly its class concurrent_vector. Reportedly it has these features:

  • Random access by index. The index of the first element is zero.
  • Multiple threads can grow the container and append new elements concurrently.
  • Growing the container does not invalidate existing iterators or indices.
Yirkha
  • 12,737
  • 5
  • 38
  • 53
  • 1
    He already has indirection, I don't think this is the right answer. – Mooing Duck May 13 '14 at 00:31
  • 1
    No need for extra libraries or frameworks. That stuff is pretty well sovable straight forward using just standard stuff. – πάντα ῥεῖ May 13 '14 at 00:45
  • This is a solution for a container with linear storage and fast O(1) indexed access like std::vector<>, but which is optimized for concurrent operation (mostly reads). Any locking on the reader's side (in simple solutions with a mutex for both reading and writing) would incur needless performance penalty. Locking only when writing is not enough with standard containers because they are not prepared for concurrent access (implementation dependent, you can't rely on it). – Yirkha May 13 '14 at 01:03
  • @Yirkha _'Locking only when writing is not enough ...'_ of course not, I've been pointing this out in my answer. I still don't see need for anything additonal, that cannot be easily build up using the available standard mechanisms. _Building blocks_ bah :-P ... – πάντα ῥεῖ May 13 '14 at 01:13
  • 1
    Thanks for taking the time to write the answer, unfortunately TBB is the last resort and seems a bit of an overkill imho. – Ælex May 13 '14 at 01:19
  • @Alex _'seems a bit of an overkill imho'_ That's what I thought as well reading this :-P ... – πάντα ῥεῖ May 13 '14 at 01:21
  • Sure, but it's not useless as certain people like to point out. If you want best performance, you can't let readers lock any mutexes --> you need a container which is thread safe in this regard --> you can't use the standard ones. – Yirkha May 13 '14 at 01:22
  • @Yirkha you're absolutely right, I don't want readers to lock, only writers (with respect to the container always). – Ælex May 13 '14 at 01:30
  • @Alex _I don't want readers to lock, only writers ..._ [This](http://www.stroustrup.com/lock-free-vector.pdf) might be relevant for you also then ... – πάντα ῥεῖ May 13 '14 at 02:00
  • @Alex Seriously: Usually readers obtain shared access, while writers aquire exlusive. There are several strategies to achieve this (e.g. using the mentioned condition variables). Certainly such has a (semaphore like count for the attached readers), and at least one mutex to aquire _read_, and another (higher priority) to aquire write locks. (Just a raw sketch) – πάντα ῥεῖ May 13 '14 at 02:05
  • 1
    @Alex *'you're absolutely right, I don't want readers to lock, only writers'* So then you need a lock-free container, as πάντα pointed out. I was in a similar situation (many reads which need to be as fast as possible, a write every now and then) and went from using a plain mutex to R/W mutex and noticed that even that still performed poorly. I had success using the scheme I outlined in the answer, and actually the container described in the linked paper by Stroustrup and Intel's `concurrent_vector` must use more or less similar techniques. It's fun to write lock-free stuff, but test it well! – Yirkha May 13 '14 at 10:54