Can the compiler reorder instructions on atomics, or do atomics serve as a memory barrier? Put again, can instructions written after an atomic instruction execute before the atomic instruction?
See the following code. If useMapA = false
is moved before mapB
is updated and the reading thread begins, we will use an invalid mapB
.
Note: the update thread only happens once every 15 minutes, so we have a very well structured flow, and a way to avoid using an expensive lock call!
std::atomic<bool> useMapA;
std::map<string, string> mapA, mapB;
public void updateMap(map<string, string>* latestMap) {
if (useMapA) {
mapB = std::move(*latestMap);
useMapA = false;
} else {
mapA = std::move(*latestMap);
useMapA = true;
}
}
inline map<string, string>& getMap() {
return useMapA ? mapA : mapB;
}
Edit: I am interested in trading away being 100% thread-safe for speed (time = money). This read function needs to run really fast. You can assume that 15 minutes is long enough to avoid race conditions that would be caused if this time was much shorter.
atomic_thread_fence(std::memory_order_acquire) prevents, that a read operation before an acquire fence can be reordered with a reading or write operation after the acquire fence.
1) A compiler fence (by itself, without a CPU fence) is only useful in two situations: To enforce memory order constraints between a single thread and asynchronous interrupt handler bound to that same thread (such as a signal handler).
Before answering your question, I would like to show, how you can easily implement the feature with std::shared_ptr and atomic operations. The following implementation is efficient and thread-safe. There is also no need for the readers to create a copy of the map.
using string_map = std::map<std::string, std::string>;
std::shared_ptr<const string_map> map;
std::shared_ptr<const string_map> getMap()
{
return std::atomic_load(&map);
}
void updateMap(string_map latestMap)
{
std::shared_ptr<const string_map> temp{
new string_map{std::move(latestMap)}};
std::atomic_store(&map, temp);
}
Now, let's take a look at your code. It's a bit more complicated. To make it easier, let's assume that updateMap is called every second instead of every 15 minutes. useMapA is initially true. The update thread executes the following statements and will be interrupted before updating the atomic flag:
if (useMapA) {
mapB = std::move(*latestMap);
Now, a reader thread only evaluates the atomic flag:
bool r1 = useMapA; // r1==true
The update thread is continued and sets the atomic flag to false. A second later, the update thread evaluates the atomic flag:
if (useMapA) { // condition is false
Now, the reader thread is continued. Both threads access mapA, and at least one thread writes to the data structure. That means, there is a data race, and that means the behavior of the program is undefined, regardless whether this data race really occurs.
What changes if the updateMap is called only every 15 minutes? Unless there happens some additional synchronization within this 15 minutes, it is still a data race, because the C++ Standard doesn't make a difference between a second and 15 minutes.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With