这3个volatile使用错误,正在毁掉你的多线程程序

写C/C++多线程程序,绕不开线程安全问题。很多程序员看到共享变量,第一反应是加个volatile关键字,以为这样就能保证线程安全了。

但这是错的。

volatile在多线程中根本不够用,它只能防止编译器优化,不能保证原子性,也不能保证内存序,用错了,程序看起来没问题,实际上随时可能崩,而且这种Bug特别难复现,往往在生产环境高并发时才暴露。

今天聊聊3个最常见的volatile使用错误。


错误1:用volatile做计数器

这是最常见的错误。很多人以为给计数器加个volatile,就能在多线程中安全使用。

看这段代码:

volatileintcounter=0;voidthread_func(){for(inti=0;i<100000;i++){counter++;// 看起来没问题?}}

看起来没问题,对吧?但这段代码在多线程环境下会出错。

为什么?因为counter++不是原子操作,它实际上是三步:从内存读取counter的值到寄存器,寄存器的值加1,把寄存器的值写回内存,而volatile只能保证每次都从内存读取、每次都写回内存、不会被编译器优化掉,但它不能保证这三步是原子的,两个线程可能同时读取、同时加1、同时写回,导致丢失更新。

两个线程同时执行counter++,可能发生这种情况:

线程A:读取counter=0 线程B:读取counter=0 线程A:计算0+1=1 线程B:计算0+1=1 线程A:写回counter=1 线程B:写回counter=1

两次加1,结果counter只增加了1,这就是数据竞争(data race),在高并发场景下,这种问题会导致计数严重不准,比如10000次加1操作,最后counter可能只有8000多,丢失了近2000次更新。

正确做法:用std::atomic

std::atomic<int>counter(0);voidthread_func(){for(inti=0;i<100000;i++){counter++;// 原子操作,线程安全}}

std::atomic保证了原子性,counter++会被编译成一条原子指令(比如x86的lock add),整个操作不可分割,不会被其他线程打断,CPU通过缓存一致性协议保证这条指令执行期间的原子性,从根本上杜绝了数据竞争。


错误2:用volatile保护共享数据结构

有些程序员以为volatile能保护复杂的数据结构。比如这样:

structSharedData{intx;inty;intz;};volatileSharedData data;voidthread1(){data.x=1;data.y=2;data.z=3;}voidthread2(){if(data.x==1&&data.y==2&&data.z==3){// 做点什么}}

这段代码有两个致命问题。

问题1:volatile不保证原子性

thread1的三次赋值不是原子的,thread2可能看到x=1, y=0, z=0这种中间状态,volatile不能把多个操作打包成原子操作,每个赋值都是独立的,线程切换可能发生在任何时候。

问题2:volatile不保证内存序

更要命的是,编译器和CPU可能会重排指令,thread1的三次赋值顺序可能变成先写z、再写x、最后写y,volatile不提供happens-before保证,它只保证每次访问都从内存读写,但不保证访问的顺序,thread2可能看到z=3, x=0, y=0,然后判断失败,但实际上thread1已经执行完了,只是顺序乱了。

正确做法:用mutex或atomic

如果是简单的标志位,用atomic:

std::atomic<bool>ready(false);voidthread1(){// 准备数据data.x=1;data.y=2;data.z=3;ready.store(true,std::memory_order_release);// 保证前面的写操作都完成}voidthread2(){if(ready.load(std::memory_order_acquire)){// 保证能看到前面的写操作// 安全使用data}}

memory_order_releasememory_order_acquire配对使用,保证了内存序,thread2看到ready=true时,一定能看到thread1对data的所有修改,这是因为release语义保证了之前的所有写操作都完成,acquire语义保证了之后的所有读操作都能看到,形成了一个同步点。

如果是复杂的数据结构,用mutex:

std::mutex mtx;SharedData data;voidthread1(){std::lock_guard<std::mutex>lock(mtx);data.x=1;data.y=2;data.z=3;}voidthread2(){std::lock_guard<std::mutex>lock(mtx);if(data.x==1&&data.y==2&&data.z==3){// 做点什么}}

mutex保证了互斥访问,同一时刻只有一个线程能访问data,不会出现中间状态,虽然性能比atomic稍差(因为涉及锁的获取释放开销,以及在高竞争场景下可能的内核态切换和上下文切换),但对于复杂数据结构来说,这是最简单、最可靠的方案。


错误3:以为volatile能防止指令重排

很多人以为volatile能防止指令重排序。这是对volatile最大的误解。

看这个经典的双重检查锁定(Double-Checked Locking):

volatileSingleton*instance=nullptr;Singleton*getInstance(){if(instance==nullptr){// 第一次检查lock();if(instance==nullptr){// 第二次检查instance=newSingleton();// 问题在这里}unlock();}returninstance;}

这段代码看起来很聪明,用volatile保证instance的可见性,用双重检查减少锁的开销,但它是错的。

问题出在new Singleton(),这个操作实际上是三步:分配内存、在内存上构造Singleton对象、把内存地址赋值给instance,而编译器和CPU可能会重排成:分配内存、把内存地址赋值给instance(此时对象还没构造完)、在内存上构造Singleton对象,如果线程A执行到第2步,instance已经不是nullptr了,但对象还没构造完,这时线程B进来,第一次检查发现instance不是nullptr,直接返回了一个未构造完的对象,程序崩溃。

volatile不能防止这种重排。它只保证对volatile变量的访问不会被优化掉,但不保证访问的顺序。

正确做法:用atomic + memory_order

std::atomic<Singleton*>instance(nullptr);Singleton*getInstance(){Singleton*tmp=instance.load(std::memory_order_acquire);if(tmp==nullptr){lock();tmp=instance.load(std::memory_order_acquire);if(tmp==nullptr){tmp=newSingleton();instance.store(tmp,std::memory_order_release);}unlock();}returntmp;}

memory_order_release保证了new Singleton()的所有操作(分配内存、构造对象)都完成后,才把地址写入instance,memory_order_acquire保证了读取instance时,能看到对象的完整状态,锁内的第二次检查也使用acquire,确保如果另一个线程已经创建了实例,当前线程能看到完全构造好的对象,这两个内存序配合使用,形成了一个完整的同步机制,彻底解决了双重检查锁定的问题。

或者更简单,用C++11的局部静态变量:

Singleton&getInstance(){staticSingleton instance;// C++11保证线程安全returninstance;}

C++11标准保证了局部静态变量的初始化是线程安全的。编译器会自动加锁,保证只初始化一次。


那volatile到底该怎么用?

说了这么多volatile不能做的事,那它到底能做什么?

volatile的设计初衷是处理"内存映射I/O"和"信号处理"。

场景1:硬件寄存器

volatileuint32_t*gpio_register=(uint32_t*)0x40020000;*gpio_register=0x01;// 写入硬件寄存器

硬件寄存器的值可能随时变化(比如GPIO输入),编译器不能假设它的值不变,volatile告诉编译器:每次都从这个地址读取,不要优化,因为硬件可能在任何时候修改这个值,编译器的优化假设(“这个变量我刚读过,值不会变”)在这里不成立。

场景2:信号处理函数

volatilesig_atomic_t flag=0;voidsignal_handler(intsig){flag=1;}intmain(){signal(SIGINT,signal_handler);while(flag==0){// 等待信号}}

信号处理函数可能在任何时候被调用,修改flag的值,volatile保证编译器不会把while (flag == 0)优化成死循环,因为编译器可能认为"flag在循环里没被修改,可以优化成if (flag == 0) { while(1); }“,但信号处理函数是异步的,编译器看不到这个修改,volatile就是告诉编译器"这个变量可能被外部修改,别优化”。

但注意:这两个场景都是单线程的。

在多线程中,volatile不够用。你需要atomic、mutex、或者其他同步原语。


总结

volatile在多线程中的三个致命缺陷:

  1. 不保证原子性- 多步操作可能被打断
  2. 不保证内存序- 指令可能被重排
  3. 不提供同步- 没有happens-before保证

记住一句话:volatile是给编译器看的,不是给CPU看的。

它只能防止编译器优化,不能防止CPU重排,也不能保证原子性。

多线程编程,该用atomic就用atomic,该用mutex就用mutex。别指望volatile能解决线程安全问题。

它真正的用途,是硬件寄存器和信号处理。仅此而已。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1175159.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

巴菲特的公司治理观:股东利益至上

巴菲特的公司治理观:股东利益至上 关键词:巴菲特、公司治理观、股东利益至上、价值投资、长期主义 摘要:本文深入探讨了巴菲特“股东利益至上”的公司治理观。从背景介绍入手,阐述了研究目的、预期读者、文档结构和相关术语。详细剖析了该治理观的核心概念、算法原理(类比…

Dnspy附加进程调试第三方App的说明

从用友工程师那学到如何用Dnspy来调试u9的dll文件。时间久了&#xff0c;不太记得具体如何操作。今天要分析一个设备的测试程序如何调用我的一个接口带来的问题&#xff0c;重新整理下思路&#xff0c;一步一步操作一遍&#xff0c;问题最终完美搞定。用的不多&#xff0c;怕忘…

提示工程架构师领域:高效提示团队打造的策略探讨

打造高效提示团队&#xff1a;提示工程架构师领域的策略指南 关键词&#xff1a;提示工程架构师、高效提示团队、策略、沟通协作、人才培养、工具选择 摘要&#xff1a;本文深入探讨了在提示工程架构师领域打造高效提示团队的策略。首先介绍了提示工程领域的背景&#xff0c;阐…

鲜花:我们的历史教育会变成什么样子?

站在学生的角度,关于历史教育的探讨。我们的历史教育会变成什么样子? 这是一个理科的时代。曾经有一句半玩笑话“从小到大生活中的几乎所有困难都源自理化能力不足”。随着国家高考与录取政策的导向,物化在考试中的…

电子发票批量提取导出合并助手

还在为发票管理而烦恼?每月堆积如山的发票需要整理?手工录入发票信息耗时费力?数据统计汇总让人头疼?发票助手为您提供一站式智能解决方案,让发票管理变得简单高效! 下载地址: https://weijiesoft.lanzouu.com/…

UART 协议规范

1. Uart介绍 通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),通常称为UART,是一种异步收发传输器,是电脑硬件的一部分。它将要传输的资料在串行通信与并行通信之间加以转换。作为把并行输入信号…

ssm493鲜活农产品商城销售系统--论文

目录具体实现截图摘要系统所用技术介绍写作提纲源码文档获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01;具体实现截图 摘要 随着互联网技术的快速发展&#xff0c;电子商务已成为农产品销售的重要渠道。鲜活农产品因其易腐性、季节性等特点&am…

基于 IPIDEA 的 GitHub 代码文件抓取与数据可视化实践(Python 实现)

基于 IPIDEA 的 GitHub 代码文件抓取与数据可视化实践(Python 实现)在实际的数据分析和工具开发过程中,GitHub 往往是一个绕不开的数据来源。无论是统计某一技术方向的项目活跃度,还是分析开源生态趋势,都需要对仓…

2026 年北京机场广告公司及机场广告牌公司综合实力排行榜单及选择建议指南:2026年北京机场广告公司及机场广告牌公司如何选?哪家好?哪家靠谱?选哪家? - Top品牌推荐

一、核心机场广告运营商 1. 艾迪亚控股集团(首选 Top 1)基本信息:始创于 1998 年,注册资金 5000 万,是全域机场广告解决方案提供商,总部位于北京,业务遍及香港、北京、上海、广州、深圳等一线城市及全国重点二三…

ssm494校园旧书交易交换平台论文

目录具体实现截图摘要系统所用技术介绍写作提纲源码文档获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01;具体实现截图 摘要 随着高校教育的发展&#xff0c;学生每年产生大量闲置教材和书籍&#xff0c;传统线下交易方式效率低、信息不对称&am…

ssm495校园视频监控系统--论文

目录具体实现截图摘要系统所用技术介绍写作提纲源码文档获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01;具体实现截图 摘要 随着校园安全问题的日益突出&#xff0c;构建高效、智能的视频监控系统成为保障校园安全的重要手段。本文基于SSM&…

挖掘大数据领域数据产品的商业价值

大数据时代的数据产品商业价值挖掘&#xff1a;从信息到价值的系统转化框架 元数据框架 标题&#xff1a;大数据时代的数据产品商业价值挖掘&#xff1a;从信息到价值的系统转化框架关键词&#xff1a;数据产品、商业价值、大数据架构、价值转化模型、应用场景、伦理考量、未来…

ssm497医院预约挂号系统--论文

目录具体实现截图摘要系统所用技术介绍写作提纲源码文档获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01;具体实现截图 摘要 随着信息技术的快速发展&#xff0c;传统医院挂号方式已无法满足患者高效、便捷的医疗需求。为提高医院挂号效率&…

张氏相机标定,不求甚解使用篇

本文记录使用张氏标定法进行使用的全过程&#xff0c;并记录最终的误差成果,为什么需要标定是因为相机本身拍照之后&#xff0c;就存在一个畸变&#xff0c;所以仅靠一个比例尺来进行推算实际距离 和 像素距离之间的比例&#xff0c;是存在很大的偏差的&#xff0c;理解一下&am…

HNU 2025年计科算法设计与分析期末考试原题

前言 感谢Smile_Laughter的共同回忆&#xff01; 一、简答题&#xff08;30分&#xff09; 1. 请简述贪心算法和动态规划算法的区别与联系。&#xff08;6分&#xff09;【提示&#xff1a;区别与联系各写2点即可】 2. 请简述队列式分支限界法和优先队列式分支限界法的区别…

开启WSL的ssh访问

开启WSL的ssh访问$(".postTitle2").removeClass("postTitle2").addClass("singleposttitle");开启WSL的ssh访问 我不打算开启Windows的ssh,只是想开WSL的ssh,因为sb微软服务器太难连了…

2026 年机场广告公司综合实力排行榜单及选择建议指南:2026年机场广告公司如何选?哪家好?哪家靠谱?选哪家? - Top品牌推荐

一、机场广告公司概述 机场广告公司是专门从事机场范围内广告媒体资源开发、运营和管理的专业机构。这些公司通过整合机场内的各种广告位资源,为广告主提供精准的品牌传播解决方案。机场作为高净值人群聚集的场所,其…

ssm488图书销售管理入库信息系统9f27q--论文

目录具体实现截图摘要系统所用技术介绍写作提纲源码文档获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&#xff01;具体实现截图 摘要 随着信息技术的快速发展&#xff0c;图书销售管理系统的信息化已成为提高企业运营效率的重要手段。本研究基于SSM&…

day138—快慢指针—删除链表的倒数第N个结点(LeetCode-19)

题目描述给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。示例 1&#xff1a;输入&#xff1a;head [1,2,3,4,5], n 2 输出&#xff1a;[1,2,3,5]示例 2&#xff1a;输入&#xff1a;head [1], n 1 输出&#xff1a;[]示例 3&#xf…

学霸同款2026 AI论文工具TOP8:本科生毕业论文神器测评

学霸同款2026 AI论文工具TOP8&#xff1a;本科生毕业论文神器测评 2026年学术写作工具测评&#xff1a;为什么需要一份权威榜单&#xff1f; 随着AI技术在学术领域的深入应用&#xff0c;越来越多的本科生开始依赖智能写作工具来提升论文效率与质量。然而&#xff0c;面对市场上…