Here is a minimal example of a threadsafe array I want to build on for a timeseries application, with the following characteristics:
- Ever-growing, and the already contained elements remain constant
- (Usually) a single writer calling
push_back
- Multiple dependent readers
Here is the corresponding implementation, or rather an early attempt of it:
template<typename T>
struct threadsafe_array
{
auto operator[](int i) const
{
return deq[i];
}
auto size() const
{
return deq_size.load(std::memory_order_acquire);
}
void push_back(T const& t)
{
std::unique_lock<std::mutex> lock(mut);
deq.push_back(t);
lock.unlock();
deq_size.fetch_add(1, std::memory_order_release);
}
private:
std::deque<T> deq;
std::atomic<int> deq_size{0};
std::mutex mut;
};
My underlying ideas:
- reads of the available elements through
operator[](int)
are carried out lock-free. - a
std::deque
is used as the underlying container because it does not invalidate concurrent reads when doing a push-back (--in contrast, astd::vector
could as it potentially does a reallocation) - the
push_back
is forwarded to the underlying deque, on which it is applied in an atomic way through locking thestd::mutex
. Thereafter, the variabledeq_size
of typestd::atomic<int>
variable is adjusted using release semantics (so that the previouspush_back
is not reordered after thefetch_add
). - if there are reads occurring in between adding an element to the deque and the adjustment of the size, they have have to get along with a smaller
size()
, i.e. as if the array had not been updated. Callingoperator[size()]
therefore does not need to be undefined behvaiour as it is forstd::deque
(but that's more an inconsistency than a feature).
Questions:
- Is this thing already threadsafe and doing what I wrote, or am I missing some points?
- Are the memory orders in the atomic operations ok, or are there better choices (e.g.
memory_order_relaxed
for the load insize()
)? - Is it preferable doing the update of the size in
push_back()
under the lock (and thus, if I see it right, limit the size difference betweensize()
and the underlyingstd::deque::size()
to only one)?