news 2026/8/19 18:14:39

C语言中的volatile到底有什么用?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言中的volatile到底有什么用?

嵌入式C编程时,有一个奇怪的关键字volatile,这到底有什么用呢?

volatile与编译器

首先来看这样一段代码:

int busy = 1; void wait() { while(busy) { ; }}

编译一下,注意,这里使用O2优化

让我们仔细看看生成的这段汇编:

wait: mov eax, DWORD PTR busy[rip].L2: test eax, eax jne .L2 retbusy: .long 1

其中L2这一段即为while循环,这段指令是经过编译器优化的,可以看到,决定能否跳出循环是通过检查寄存器eax来完成的,而没有检查变量busy所在内存的真实内容。

注意,对于这段代码来说这里的优化是正确的,但问题是如果还有其它代码修改了变量busy,那么这里的优化会导致其它代码对变量busy的修改根本就不能生效,就像这样:

int busy = 1;// 该函数在A线程中执行void wait() { while(busy) { ; }}// 该函数在B线程中执行void signal() { busy = 0;}

如果wait函数中while循环对应的机器指令仅仅从寄存器中读取数据那么即使B线程的signal函数修改了busy变量也不能让wait函数从循环中跳出来。

如果你对busy变量使用volatile修饰,生成的指令就变成这样了:

wait:.L2: mov eax, DWORD PTR busy[rip] test eax, eax jne .L2 retbusy: .long 1

注意看此时L2这一段,每次都从busy变量所在的内存中读取数据并存放在eax,然后再去判断,这样就能确保每次都能读取到busy变量的最新值。

实际上你可以把寄存器eax当做busy所在内存的cache,当cache(寄存器)和内存中的数据一致时不会有任何问题,但当cache与内存中的数据不一致时(也就是内存已被更新但cache保存的还是旧数据),程序的运行往往出乎预料。

除了多线程的例子,还有一类就是signal handler以及硬件修改该变量(用C语言与硬件交互式时经常遇到),如果编译器生成文章开头那样的指令那么等待线程将检测不到signal handler或者硬件对变量的修改。

因此在这里我们需要告诉编译器:“不要耍小聪明,不要只从寄存器中读数据,这个变量可能在其它地方已经被修改了,使用时从内存中获取最新数据”。

现在是时候简单总结一下了,volatile仅仅阻止编译器试图去优化对变量的读取操作

volatile与多线程

一定要注意volatile仅仅确保变量的可见性,但和变量的原子访问没有半毛钱关系,这是两个完全不同的任务

假设有一个非常复杂的结构体struct foo:

struct data { int a; int b; int c; ...};volatile struct data foo; void thread1() { foo.a = 1; foo.b = 2; foo.c = 3; ...}void thread2() { int a = foo.a; int b = foo.b; int c = foo.c; ...}

你仅仅用volatile去修饰变量foo只是确保了当该变量被thread1修改后我们能在thread2中读取到最新值,但是这解决不了多线程并发读写需要原子访问foo的问题

确保变量原子性访问一般都采用锁,当使用锁时,锁本身就包含了volatile提供能力,即,确保变量的可见性,因此当使用锁时没有必要使用volatile。

volatile与memory order

有的同学可能会想如果我想用volatile修饰的变量没有那么复杂,仅仅是一个int,就像这样:

volatileint busy = 0;

A线程读取busy变量,B线程更新busy变量,当A检测到busy变化后执行特定操作,这样可行吗?既然通过volatile修饰后可以确保每次都从内存中读取busy,那么应该可以这样使用吧。

然而,计算机在概念上可能相对简单些,但在工程实践中是复杂的。

我们知道由于CPU与内存之间的速度差异非常大,CPU与内存之间有一层cache,CPU其实并没有直接读取内存,cache的存在会让问题复杂起来,限于篇幅与本文主题这里不再展开。

为优化内存读写,CPU可能会对内存读写操作进行指令重排,reordering,带来的后果就是:假设在线程1中先后执行第N行代码与第N+1行代码,但在线程2看来却是第N+1行代码先生效,假设X的初始值为0,Y的初始值为1:

线程1 线程2X = 10 if (!busy)busy = 0; Y = X;

当线程2检测到busy为0后读取X的值,此时读取到的X值可能为0。

为解决这一问题,我们需要的不是volatile,volatile解决不了reordering问题,我们需要的是内存屏障,memory barrier。

内存屏障是一类机器指令,该指令对处理器在该屏障指令之前与之后的内存操作进行了限制,确保不会出现重排问题。

而内存屏障带来的效果依然能够涵盖volatile提供的功能,因此也不需要volatile。

------------END------------

不靠 MCU,用 FPGA + DAC 实现可调信号源

嵌入式多线程从"能跑"到"稳定"的关键一步!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/6 18:41:15

29、业务数据图与数据流图全解析

业务数据图与数据流图全解析 业务数据图中的关系与基数 在业务数据建模中,关系的表示至关重要。以学生和课程为例,学生可以选择任意数量的课程,而课程也可以有零到无限数量的学生报名,这体现了学生与课程之间的多对多关系,如下所示: graph LRclassDef process fill:#…

作者头像 李华
网站建设 2026/7/29 4:03:44

33、状态表的使用与创建指南

状态表的使用与创建指南 1. 状态表模板 状态表以网格形式呈现,顶行列出所有状态,首列重复这些状态。网格中每个单元格的值表示从该行的初始状态到该列的目标状态是否存在有效的转换。首列的状态集标记为“初始状态”,顶行的状态集标记为“目标状态”,以展示转换流程的顺序…

作者头像 李华
网站建设 2026/8/10 23:32:12

42、项目需求建模与管理全解析

项目需求建模与管理全解析 在项目管理与开发过程中,需求的准确把握和有效管理是项目成功的关键。本文将深入探讨项目需求建模与管理的相关知识,包括各种模型的定义、创建方法、应用场景以及它们之间的关系。 1. 项目需求基础概念 需求定义 :需求是项目开发的基础,明确规…

作者头像 李华
网站建设 2026/7/31 20:10:15

20、对话框控件与文档视图架构详解

对话框控件与文档视图架构详解 1. 列表框控件与组合框控件 在开发过程中,我们常常会用到列表框和组合框控件。 1.1 列表框控件 有时候,了解当前所选的所有项是很有用的。要实现这一点,需要使用 CListBox 类的功能。 CListBox 包含几个用于获取和更改多选列表框选择的…

作者头像 李华
网站建设 2026/7/30 6:45:25

27、深入理解 Git 子模块管理:方法与实践

深入理解 Git 子模块管理:方法与实践 1. 确定合并分支 在合并操作之后,我们可以通过查看提交信息来确定合并的是哪些分支的 HEAD 。示例如下: Merge: 6c9fac5... 5760a6b...这里的 6c9fac5... 和 5760a6b... 分别对应 HEAD^1 和 HEAD^2 。例如: commit 576…

作者头像 李华
网站建设 2026/7/31 17:22:30

35、状态图与报表表:使用指南与操作要点

状态图与报表表:使用指南与操作要点 1. 状态图的使用场景 1.1 适用情况 状态表和状态图在很多情况下都可以结合使用。下面的表格展示了它们各自适用的场景: | 模型 | 使用场景 | | — | — | | 状态表 | 当你需要确保考虑到每个状态和可能的转换时使用 | | 状态图 | 当…

作者头像 李华