I encountered the following implementation of Singleton's get_instance
function:
template<typename T>
T* Singleton<T>::get_instance()
{
static std::unique_ptr<T> destroyer;
T* temp = s_instance.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);
if (temp == nullptr)
{
std::lock_guard<std::mutex> lock(s_mutex);
temp = s_instance.load(std::memory_order_relaxed);/* read current status of s_instance */
if (temp == nullptr)
{
temp = new T;
destroyer.reset(temp);
std::atomic_thread_fence(std::memory_order_release);
s_instance.store(temp, std::memory_order_relaxed);
}
}
return temp;
}
And I was wondering - is there any value in the acquire and release memory barriers there? As far as I know - memory barriers are aimed to prevent reordering of memory operations between 2 different variables. Let's take the classic example:
(This is all in pseudo-code - don't be caught on syntax)
# Thread 1
while(f == 0);
print(x)
# Thread 2
x = 42;
f = 1;
In this case, we want to prevent the reordering of the 2 store operations in Thread 2, and the reordering of the 2 load operations in Thread 1. So we insert barriers:
# Thread 1
while(f == 0)
acquire_fence
print(x)
# Thread 2
x = 42;
release_fence
f = 1;
But in the above code, what is the benefit of the fences?
EDIT
The main difference between those cases as I see it is that, in the classic example, we use memory barriers since we deal with 2 variables - so we have the "danger" of Thread 2 storing f
before storing x
, or alternatively having the danger in Thread 1 of loading x
before loading f
.
But in my Singleton code, what is the possible memory reordering that the memory barriers aim to prevent?
NOTE
I know there are other ways (and maybe better) to achieve this, my question is for educational purposes - I'm learning about memory barriers and curious to know if in this particular case they are useful. So all other things here not relevant for this manner please ignore.