如何解决C++20 线程可能永远等待 std::atomic
考虑以下示例代码,其中线程 A 将函数推入队列,线程 B 在从队列中弹出时执行这些函数:
std::atomic<uint32_t> itemCount;
//Executed by thread A
void run(std::function<void()> function) {
if (queue.push(std::move(function))) {
itemCount.fetch_add(1,std::memory_order_acq_rel);
itemCount.notify_one();
}
}
//Executed by thread B
void threadMain(){
std::function<void()> function;
while(true){
if (queue.pop(function)) {
itemCount.fetch_sub(1,std::memory_order_acq_rel);
function();
}else{
itemCount.wait(0,std::memory_order_acquire);
}
}
}
其中 queue
是一个并发队列,它有一个 push
和一个 pop
函数,每个函数返回一个 bool
,指示给定的操作是否成功。因此,push
如果已满则返回 false
,如果为空则 pop
返回 false
。
现在我想知道代码是否在所有情况下都是线程安全的。
假设线程 B 的 pop
失败并即将调用 std::atomic<T>::wait
。同时,线程 A 推送一个新元素,而线程 B 检查初始等待条件。由于 itemCount
尚未更改,因此失败。
紧接着,线程 A 增加计数器并尝试通知一个等待的线程(尽管线程 B 还没有在内部等待)。线程 B 最终等待原子,导致线程由于丢失信号而永远不会再次唤醒尽管队列中有一个元素。只有当一个新元素被推入队列时才会停止,通知 B 继续执行。
我无法手动重现这种情况,因为时机几乎不可能正确。
这是一个严重的问题还是不可能发生?为了解决这种罕见的情况,确实存在哪些(最好是原子的)替代方案?
编辑: 顺便提一下,队列不是阻塞的,只使用原子操作。
我问的原因是我不明白如何实现原子 wait
操作。虽然标准说整个操作是原子的(由加载 + 谓词检查 + 等待组成),但在我使用的实现中,std::atomic<T>::wait
的实现大致如下:
void wait(const _TVal _Expected,const memory_order _Order = memory_order_seq_cst) const noexcept {
_Atomic_wait_direct(this,_Atomic_reinterpret_as<long>(_Expected),_Order);
}
其中 _Atomic_wait_direct
定义为
template <class _Ty,class _Value_type>
void _Atomic_wait_direct(
const _Atomic_storage<_Ty>* const _This,_Value_type _Expected_bytes,const memory_order _Order) noexcept {
const auto _Storage_ptr = _STD addressof(_This->_Storage);
for (;;) {
const _Value_type _Observed_bytes = _Atomic_reinterpret_as<_Value_type>(_This->load(_Order));
if (_Expected_bytes != _Observed_bytes) {
return;
}
__std_atomic_wait_direct(_Storage_ptr,&_Expected_bytes,sizeof(_Value_type),_Atomic_wait_no_timeout);
}
}
我们可以清楚地看到,有一个原子负载以指定的内存顺序来检查原子本身的状态。但是,我不明白如何将整个操作视为原子操作,因为在调用 __std_atomic_wait_direct
之前有一个比较。
对于条件变量,谓词本身由互斥锁保护,但是这里的原子本身是如何保护的?
解决方法
以下是标准的内容:
[intro.races]/4 对特定原子对象 M
的所有修改都以某种特定的总顺序发生,称为 M
的修改顺序。
[atomics.wait]/4 对原子对象 M
上的原子等待操作的调用有资格通过调用M
上的原子通知操作,如果 X
上存在副作用 Y
和 M
使得:
(4.1) — 原子等待操作在观察到 X
,
的结果后被阻塞
(4.2) — X
在 Y
的修改顺序中位于 M
之前,并且
(4.3) — Y
发生在调用原子通知操作之前。
您假设以下场景:
-
itemCount
的当前值为零,无论是初始初始化还是之前的fetch_sub
。 -
wait
加载itemCount
并观察值 0。 - 另一个线程调用
fetch_add
和notify_one
-
wait
会阻塞,因为它认为现在已经过时的值为 0。
在这种情况下,M
是 itemCount
,X
是将值变为 0 的旧 fetch_sub
(我们假设它发生在很久以前并且是正确可见的到所有线程)和 Y
是将值更改为 1 的 fetch_add
。
标准说 wait
调用(跨越第 2 步和第 4 步)实际上有资格被第 3 步的 notify_one
调用解除阻塞。确实:
(4.1) - wait
在观察到 itemCount == 0
后被阻塞(如果没有,则问题不会出现)。
(4.2) - fetch_sub
在 fetch_add
的修改顺序中位于 itemCount
之前(假设 fetch_sub
发生在很久以前)。
(4.3) - fetch_add
发生在之前(实际上是排序在之前)notify_one
;它们被同一个线程一个接一个地调用。
因此,符合要求的实现必须首先不允许 wait
阻塞,或者让 notify_one
唤醒它;它不允许错过通知。
本次讨论中内存顺序唯一的地方是在(4.1)中,“原子等待操作在观察X
的结果后被阻塞”。也许 fetch_add
实际上发生在 wait
之前(通过挂钟),但对 wait
不可见,因此它无论如何都会阻塞。但对结果无关紧要——要么 wait
观察 fetch_add
的结果并且根本不阻塞;或者它观察到旧的 fetch_sub
的结果并阻塞,但是需要 notfiy_one
来唤醒它。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。