I'm searching for some guiding principles to apply when using container data structures with Boost.ASIO. The Boost.ASIO documentation describes how strand
objects can be used to provide serialized access to shared resources without explicit thread synchronization. I am looking for a systematic way to apply strand
synchronization to:
- STL (or STL-like) containers (e.g.,
std::deque
,std::unordered_map
); and - wait-free containers such as
boost::lockfree::spsc_queue
orfolly::ProducerConsumerQueue
.
My questions are listed below. I should mention that my main questions are 1-3, but with justification I am also prepared to accept "These questions are moot/misguided" as an answer; I elaborate on this in question 4.
To adapt an arbitrary STL container for safe synchronized use, is it sufficient to perform all its operations through a
strand
instance?To adapt a wait-free read-write container for synchronized, concurrent use, is it sufficient to wrap its operations through two distinct
strand
s, one for read operations and one for write operations? This question hints at a "yes", although in that use case the author describes using astrand
to coordinate producers from several threads, while presumably only reading from one thread.If the answer to 1-2 above is yes, should the
strand
just manage operations on the data structure through calls toboost::asio::post
?
To give an idea of the issue in 3., here is a snippet from the chat client example:
void write(const chat_message& msg)
{
boost::asio::post(io_context_,
[this, msg]()
{
bool write_in_progress = !write_msgs_.empty();
write_msgs_.push_back(msg);
if (!write_in_progress)
{
do_write();
}
});
}
Here, write_msgs_
is a queue of chat messages. I ask because this is a bit of a special case where the call to post
can potentially invoke a composed async operation (do_write
). What if I just wanted to push or pop from a queue? To take a highly simplified example:
template<typename T>
class MyDeque {
public:
push_back(const T& t);
/* ... */
private:
std::deque<T> _deque;
boost::asio::io_context::strand _strand
};
Then should MyDeque::push_back(const T& t)
just call
boost::asio::post(_strand, [&_deque]{ _deque.push_back(t); })
and similarly for other operations? Or is boost::asio::dispatch
a more appropriate option?
- Finally, I know that there are a plenty of robust implementations of concurrent vectors, hash maps, etc. (e.g., Intel Thread Building Blocks Containers). But it seems that under restricted use cases (e.g., just maintaining a list of active chat participants, storing recent messages, etc.) that the force of a fully concurrent vector or hash map may be a bit overkill. Is this indeed the case or would I be better off just using a fully concurrent data structure?