当前位置: 首页 > 知识库问答 >
问题:

C 20线程可能永远在等待std::atomic

晏炳
2023-03-14

考虑以下示例代码,其中线程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);
        }

    }

}

其中队列是一个并发队列,它有一个推送和一个pop函数,每个返回一个bool指示给定操作是否成功。因此,如果推送已满,则返回false,如果为空,则返回pop

现在我想知道代码是否在所有情况下都是线程安全的。让我们假设线程B的pop失败并即将调用std::atomic

紧接着,线程A增加计数器并尝试通知一个等待线程(尽管线程B还没有在内部等待)。线程B最终等待原子,导致线程由于丢失信号而永远不会再次醒来,尽管队列中有一个元素。这只会在队列中推送新元素时停止,通知B继续执行。

我无法手动重现这种情况,因为时间几乎不可能正确。

这是一个严重的问题还是不可能发生?为了解释这种罕见的情况,存在哪些(最好是原子)替代方案?

编辑:顺便提一下,队列没有阻塞,只利用原子操作。

我问的原因是我不明白如何实现原子的等待操作。虽然标准说整个操作是原子的(包括加载谓词检查等待),但在实现中,我使用的是原子的

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);
}

其中,原子等待直接定义为

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之前有一个比较。

对于条件变量,谓词本身由互斥锁保护,但是原子本身在这里是如何保护的呢?


共有2个答案

艾子石
2023-03-14

首先要指出可能导致混淆的原因:函数\ustd\u atomic\u wait\u direct本身以原子方式进行比较和阻塞。它可以归结为对WaitOnAddress的调用,或者使用条件变量,在保存相关互斥体的同时进行比较。

if(\u Expected\u bytes!=\u Observed\u bytes)只是一个优化。如果变量已经不同于预期值,我们可以立即返回,而无需对底层原子函数进行相对昂贵的调用。它不需要是原子的块,可以完全删除而不影响语义。

弘浩瀚
2023-03-14
匿名用户

标准是这样说的:

【简介】/4对特定原子对象的所有修改都以某种特定的总顺序发生,称为对M的修改顺序。

[原子.等待]/4如果对原子对象的原子等待操作调用M存在副作用,则可以通过对M的原子通知操作调用来解除对原子对象的原子等待操作的阻止:
(4.1)-观察到X的结果后,原子等待操作已被阻止,
(4.2)-XY之前的修改顺序为M,而
(4.3)-Y发生在调用原子通知操作之前。

您假设以下场景:

  1. 原始初始化或之前的fetch\u sub的当前值为零
  2. <代码>等待加载<代码>项目计数并观察值0
  3. 其他线程调用fetch\u add和notify\u one
  4. 等待会阻塞,因为它认为现在过时的值为0

在这种情况下,M是itemCount,X是旧的fetch\u sub,它将值设置为0(我们假设它发生在很久以前,并且对所有线程都正确可见),Y是将值更改为1的fetch\u add。

标准说等待调用(跨越步骤2和4)实际上有资格被步骤3上的notify_one调用解除阻塞。事实上:

(4.1)-等待在观察到itemCount==0后已阻塞(如果没有,则问题失败)。
(4.2)-fetch_subitemCount的修改顺序中位于fetch_add之前(假设fetch_sub发生在很久以前)。
(4.3)-fetch_add发生在之前(事实上,是在之前排序的)notify_one;它们被同一个线程一个接一个地调用。

因此,符合规范的实现必须首先不允许等待阻塞,或者让notify_one唤醒它;它不能允许错过通知。

本讨论中唯一显示内存顺序的地方是(4.1),“在观察到X的结果后,原子等待操作已被阻止”。也许,fetch\U add(提取添加)实际上发生在wait(按挂钟)之前,但对wait(等待)不可见,所以它还是被阻塞了。但这与结果无关-要么等待,要么观察fetch\u add的结果,根本不阻塞;或者,它观察旧的fetch\u sub和块的结果,但需要notfiy\u one将其唤醒。

 类似资料:
  • 如果吊舱已经处于状态,我还希望命令立即返回。但这不会发生。 kubectl wait不是我要找的命令吗?

  • 问题内容: 我正在为我的ubuntu服务器(针对我的多客户端匿名聊天程序)实现一种简单的线程池机制,并且需要使我的工作线程进入睡眠状态,直到需要执行一项工作(以函数指针和参数的形式) 。 我当前的系统即将关闭。我(工人线程正在)问经理是否有工作可用,以及是否有5毫秒没有睡眠。如果存在,请将作业添加到工作队列中并运行该函数。糟糕的循环浪费。 什么我 喜欢 做的是做一个简单的事件性的系统。我正在考虑有

  • 更新假设副本是执行Run时在“等待调试器”中遇到的问题,而执行Debug时在“等待调试器”中遇到的问题,产生问题的步骤不同,解决方案也不同。 每当我尝试使用Android Studio的调试功能时,运行状态总是停留在: 当我正在调试的设备(三星Galaxy S3 Android 4.3)将显示 从Android Studio0.8.8一直到1.0都是这样。在同一台计算机上,我可以在同一台设备上使用

  • 问题内容: 我有以下情况: 为了运行算法,我必须运行多个线程,并且每个线程都会在死之前设置一个实例变量x。问题是这些线程不会立即返回: 我应该使用等待通知吗?还是我应该嵌入一个while循环并检查是否终止? 感谢大家! 问题答案: 创建一些共享存储来保存每个线程的值,或者如果足够的话,只存储总和。使用a 等待线程终止。每个线程完成后都会调用,您的方法将使用该方法来等待它们。 编辑: 这是我建议的方

  • 我们有一个由2个节点a和B组成的集群。 如果我们在节点a上阻止线程的执行(我是通过远程连接到我的VM并放置断点来执行的),那么节点B认为节点a被分段(这是预期的行为)。 在执行上述场景后,节点A上的任何缓存查找都会被卡住,我们永远不会从IGniteCache中获得结果。 我已经编写了一个测试应用程序来重现这个问题。复制这个问题并不困难,但我已经尽力在readme.md中提供了指导 如果你仍然不清楚

  • 嗨,我正在做一个项目,我已经达到了我非常困的部分。我试图寻找方法来学习如何在繁忙的等待中编写 while 循环,但我没有找到任何东西,我的代码只是作为无限循环运行。有人可以帮助我解释一个繁忙的等待循环应该如何工作,并帮助我打破这个无限循环吗? 该项目希望做到以下几点:早上,学生醒来后(这需要一段随机的时间),他会去洗手间为新的上学日做准备。如果浴室已经客满,学生需要Rest一下(使用yield()