Assume a repeated acquire operation, that tries to load or exchange a value until the observed value is the desired value.
Let's take cppreference atomic flag example as a starting point:
void f(int n)
{
for (int cnt = 0; cnt < 100; ++cnt) {
while (lock.test_and_set(std::memory_order_acquire)) // acquire lock
; // spin
std::cout << "Output from thread " << n << '\n';
lock.clear(std::memory_order_release); // release lock
}
}
Now let's consider enhancements to this spinning. Two well-known are:
- Don't spin forever, instead go to OS wait at some point;
- Use an instruction, such as
pauseoryieldinstead of no-operation spinning.
I can think of a third, and I'm wondering if it ever makes sense.
We can use std::atomic_thread_fence for acquire semantic:
void f(int n)
{
for (int cnt = 0; cnt < 100; ++cnt) {
while (lock.test_and_set(std::memory_order_relaxed)) // acquire lock
; // spin
std::atomic_thread_fence(std::memory_order_acquire); // acquire fence
std::cout << "Output from thread " << n << '\n';
lock.clear(std::memory_order_release); // release lock
}
}
I expect that to be no change for x86.
I'm wondering:
- Is there benefits or drawbacks from this change on platforms where there is a difference (ARM)?
- Is there any interference with the decision to use or not to use
yieldinstruction?
I'm not only interested in atomic_flag::clear / atomic_flag::test_and_set pair, I'm also interested in atomic<uint32_t>::store / atomic<uint32_t>::load pair.
Possibly changing to relaxed load could make sense:
void f(int n)
{
for (int cnt = 0; cnt < 100; ++cnt) {
while (lock.test_and_set(std::memory_order_acquire)) // acquire lock
while (lock.test(std::memory_order_relaxed))
YieldProcessor(); // spin
std::cout << "Output from thread " << n << '\n';
lock.clear(std::memory_order_release); // release lock
}
}