https://gcc.gnu.org/bugzilla/show_bug.cgi?id=126235

--- Comment #2 from Wolfgang Hoffmann <wolfgang.hoffmann at besi dot com> ---
Thanks for commenting.

It's not obviously a classic priority inversion in the sense that a
high-priority thread is forced to wait because a lower-priority thread holds a
resource it needs, and an intermediate-priority thread prevents the
lower-priority thread from running and releasing that resource. 

Here, there is no intermediate-priority thread needed to reproduce the stall.
At the core of the test, two threads (getter and setter) are sufficient to
reproduce the stall. The third thread (watchdog) in my reproducer is just for
convenience to end the test on stall, as otherwise the system might be hard to
recover with the spinning SCHED_FIFO thread. The stall still happens when
removing the watchdog thread from the reproducer.

To my understanding, the load/store operations should be atomic in the sense
that concurrent action is serialized in arbitrary order, but done atomically.
The deprecated std::atomic_load/store free functions do so, but the c++20
std::atomic<std::shared_ptr>::load/store implementation behaves differently and
stalls when the low-prio getter is preempted e.g. here:

#0  std::_Sp_atomic<std::shared_ptr<int> >::_S_add_ref (__p=0xdccde95e2f4c6300)
at /usr/include/c++/15/bits/shared_ptr_atomic.h:532
#1  0x0000555555559266 in std::_Sp_atomic<std::shared_ptr<int> >::load
(this=0x7fffffffe2d0, __o=std::memory_order::seq_cst) at
/usr/include/c++/15/bits/shared_ptr_atomic.h:569
#2  0x0000555555558996 in std::atomic<std::shared_ptr<int> >::load
(this=0x7fffffffe2d0, __o=std::memory_order::seq_cst) at
/usr/include/c++/15/bits/shared_ptr_atomic.h:657
#3  0x0000555555556808 in operator() (__closure=0x7fffffffe2a8) at
cpp20atomic.cpp:66

by the higher-prio setter that ends up spinning on the internal lock here:

#0  std::__detail::__thread_relax () at
/usr/include/c++/15/bits/atomic_wait.h:149
#1  0x0000555555559aa7 in std::_Sp_atomic<std::shared_ptr<int>
>::_Atomic_count::lock (this=0x7fffffffe2d8, __o=std::memory_order::acquire) at
/usr/include/c++/15/bits/shared_ptr_atomic.h:449
#2  0x0000555555559324 in std::_Sp_atomic<std::shared_ptr<int> >::swap
(this=0x7fffffffe2d0, __r=std::shared_ptr<int> (use count 3, weak count 0) =
{...}, __o=std::memory_order::seq_cst)
    at /usr/include/c++/15/bits/shared_ptr_atomic.h:577
#3  0x0000555555558a1d in std::atomic<std::shared_ptr<int> >::store
(this=0x7fffffffe2d0, __desired=std::shared_ptr<int> (use count 3, weak count
0) = {...}, __o=std::memory_order::seq_cst)
    at /usr/include/c++/15/bits/shared_ptr_atomic.h:665
#4  0x0000555555556870 in operator() (__closure=0x7fffffffe2b0,
p=std::shared_ptr<int> (use count 3, weak count 0) = {...}) at
cpp20atomic.cpp:67

It looks as if the setter remains in user-space retry/spin code rather than
blocking in a way that would let the getter run and finish the atomic load.

>From a user point of view, I use std::atomic to serialize exactly those
concurrent accesses, and expect the libstdc++ specialization for
std::shared_ptr to behave robustly even under this scheduler setup. Am I
missing something?

Reply via email to