Streaming settings

I am writing several classes of settings that can be accessed worldwide in a multi-threaded application. I will read these settings very often (therefore, read access should be fast), but they are not written very often.

For primitive data types, it looks like boost::atomic offers what I need, so I came up with something like this:

 class UInt16Setting { private: boost::atomic<uint16_t> _Value; public: uint16_t getValue() const { return _Value.load(boost::memory_order_relaxed); } void setValue(uint16_t value) { _Value.store(value, boost::memory_order_relaxed); } }; 

Question 1: I'm not sure about memory ordering. I think in my application I really don't like the memory ordering (right?). I just want to make sure getValue() always returns an intact value (either old or new). Are the memory order settings correctly configured?

Question 2: Is this approach used using boost::atomic for this kind of synchronization? Or are there other designs that provide better reading performance?

I will also need some more complex types of settings in my application, for example std::string or, for example, the list boost::asio::ip::tcp::endpoint s. I consider all these settings to be unchanged. Therefore, as soon as I set the value using setValue() , the value itself ( std::string or list of endpoints) no longer changes. So again, I just want to make sure that I get either the old value or the new value, but not some corrupted state.

Question 3: Does this approach work with boost::atomic<std::string> ? If not, what are the alternatives?

Question 4: What about more complex types of settings, such as a list of endpoints? Would you recommend something like boost::atomic<boost::shared_ptr<std::vector<boost::asio::ip::tcp::endpoint>>> ? If not, what would be better?

+6
source share
3 answers

Q1, That's right, if you are not trying to read any common non-atomic variables after reading the atom. Memory items only synchronize access to non-atomic variables that can occur between atomic operations

Q2 I do not know (but look below)

Q3 Should work (if compiled). but

  atomic<string> 

possibly not blocked

Q4 Should work, but, again, the implementation cannot be blocked (the implementation of lockfree shared_ptr is a complex task and a field marked by patents).

Thus, it is likely that blocking reader-writers (as suggested by Damon in the comments) can be simpler and even more effective if your configuration includes data larger than 1 machine word (for which the atomic memory of the processor usually works)

[EDIT] However

 atomic<shared_ptr<TheWholeStructContainigAll> > 

may make some sense even when unlocked: this approach minimizes the likelihood of a collision for readers who need more than one coherent value, although the author must make a new copy of the entire “parameter page” every time she changes something.

+2
source

For question 1, the answer is "dependent, but probably not." If you really only care that one value is not distorted, then yes, this is normal and you also don't need the memory order.
However, as a rule, this is a false assumption.

For questions 2 , 3, and 4, yes, it will work, but it will most likely use a lock for complex objects such as string (internally, for every access, without your knowledge). Only small objects, the size of which is approximately equal to one or two pointers, can usually be accessed / changed atomically without blocking. It also depends on your platform.

The big difference is that one of them successfully updates one or two values ​​atomically. Suppose you have left and right values ​​that limit the left and right borders, where the task will do some processing in the array. Suppose they are 50 and 100 respectively, and you change them to 101 and 150, each atomically. Thus, another thread picks up a change from 50 to 101 and starts performing calculations, sees that 101> 100, finishes and writes the result to a file. After that, you change the name of the output file, again, atomically.
Everything was atomic (and therefore more expensive than usual), but none of this was useful. The result is still erroneous and was also written in the wrong file.
This may not be a problem in your particular case, but usually it (and your requirements may change in the future). Usually you really want the complete set of changes to be atomic.

However, if you have either many or complex (or, like many, complex) updates like this, you might want to use one large (reader / writer) lock for the entire configuration anyway, so as it is more efficient than acquiring and releasing 20 or 30 locks or performing 50 or 100 atomic operations. However, keep in mind that in any case, blocking will greatly affect performance.

As stated in the comments above, I would rather make a deep copy of the configuration from a single thread that changes the configuration, and plan on updating the reference (common pointer) used by consumers as common tasks. This copy-modify-publish approach is a bit like working with MVCC databases (they also have the problem that locking kills their performance).

Changing the copy means that only readers get access to any general state, so synchronization is not required for readers or for any author. Reading and writing fast. The configuration set is exchanged only at clearly defined points at the moments when the set is guaranteed to be in a complete, consistent state, and the flows are guaranteed not to do anything else, so there can be no ugly surprises.

A typical task-driven application will look something like this (in C ++ pseudo-code):

 // consumer/worker thread(s) for(;;) { task = queue.pop(); switch(task.code) { case EXIT: return; case SET_CONFIG: my_conf = task.data; break; default: task.func(task.data, &my_conf); // can read without sync } } // thread that interacts with user (also producer) for(;;) { input = get_input(); if(input.action == QUIT) { queue.push(task(EXIT, 0, 0)); for(auto threads : thread) thread.join(); return 0; } else if(input.action == CHANGE_SETTINGS) { new_config = new config(config); // copy, readonly operation, no sync // assume we have operator[] overloaded new_config[...] = ...; // I own this exclusively, no sync task t(SET_CONFIG, 0, shared_ptr<...>(input.data)); queue.push(t); } else if(input.action() == ADD_TASK) { task t(RUN, input.func, input.data); queue.push(t); } ... } 
+2
source

For something more substantial than a pointer, use a mutex. The tbb library (opensource) supports the concept of read-write mechanisms that allow multiple simultaneous readers, see the documentation .

+2
source

Source: https://habr.com/ru/post/957189/


All Articles