Search code examples
c++multithreadingatomicproducer-consumercompare-and-swap

Better way in C++ to keep a big plain struct atomic?


I have a big plain struct without any methods. It includes many fields and another container(std::vector). I need to make it atomic, so as to let one producer thread and many consumer threads can access it concurrently. consumers don't modify the data, they just read the data. there is only one producer that adds/modifys(don't remove) the data, so it must keep the integrity/consistency of the data that can be seen by consumers whenever.

I think that there are two solutions can resolve it, but I don't know which one is better.

Method 1:

struct Single_t {
   size_t idx;
   double val;
}
using Details_t = std::vector<Single_t>;

struct BigStruct_1 {
   std::string      id;
   size_t           count;
   Details_t        details;
   std::atomic_bool data_ok { false };
};
std::map<std::string, BigStruct_1> all_structs1;

Method 2:

// Single_t and Details_t are same as the ones in Method1.
struct BigStruct_2 {
   std::string id;
   size_t      count;
   Details_t   details;
};
std::map<std::string, std::atomic<BigStruct_2> > all_structs2;

I prefer Methods2 up to now because I don't need to check whether data_ok in everywhere of consumer's codes. Infact consumer holds a pointer to a single BigStructs_2, I want the data can be accessed anytime(the data will not be removed). Fortunately there is only one producer in my system, so the producer can simply overwrite the data in one step: std::atomic<BigStruct_2>::store( tmp_pod );, So I don't need to check whether or not the data is ready/OK(It should be always OK) when a consumer is reading the data, and I also don't need to use std::atomic::compare_exchange_xxx in this case, so it isn't necessary to offer a comparing algorithm between two BigStruct_2 objs. Am I right?

If I use Method1, when I need to modify the data I have to code as following:

auto& one_struct = all_structs1["some_id"];
one_struct.data_ok.store( false, memory_order_release );
// change the data....
one_struct.data_ok.store( true, memory_order_release );

and check if data_ok everywhere in consumer's codes... it's ugly and dangerous(easy to forget to check).

Any recomments/hints will be appreciated!


Solution

  • I suggest you make your BigStructs effectively immutable, and store them like this:

    std::map<std::string, std::shared_ptr<BigStruct_2> > all_structs2;
    

    In order to make an update, the writer-thread would allocate a new BigStruct_2 (using std::make_shared or whatever), set it equal to the object it wants to modify, then modify the new BigStruct_2, then insert the new shared_ptr<BigStruct_2> into the map.

    Then whenever a reader-thread wants to read a BigStruct_2 it can do so safely by holding a local std::shared_ptr<BigStruct_2> that it retrieved from the std::map. Since your writer-thread is careful to never modify a BigStruct_2 that it has already inserted it into the map, there is no chance that a reader-thread will be wrong-footed by a BigStruct_2's contents changing while it is in the process of reading it.

    Note that none of the above makes accesses to the std::map itself thread-safe, so you'd still need to synchronize all of those with a mutex. Those actions should be quite fast, though, so I doubt that will be a problem.