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

如何解决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 上存在副作用 YM 使得:
(4.1) — 原子等待操作在观察到 X,
的结果后被阻塞 (4.2) — XY 的修改顺序中位于 M 之前,并且
(4.3) — Y 发生在调用原子通知操作之前。

您假设以下场景:

  1. itemCount 的当前值为零,无论是初始初始化还是之前的 fetch_sub
  2. wait 加载 itemCount 并观察值 0。
  3. 另一个线程调用 fetch_addnotify_one
  4. wait 会阻塞,因为它认为现在已经过时的值为 0。

在这种情况下,MitemCountX 是将值变为 0 的旧 fetch_sub(我们假设它发生在很久以前并且是正确可见的到所有线程)和 Y 是将值更改为 1 的 fetch_add

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

(4.1) - wait 在观察到 itemCount == 0 后被阻塞(如果没有,则问题不会出现)。
(4.2) - fetch_subfetch_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 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 &lt;select id=&quot;xxx&quot;&gt; SELECT di.id, di.name, di.work_type, di.updated... &lt;where&gt; &lt;if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 &lt;property name=&quot;dynamic.classpath&quot; value=&quot;tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-