个人结论:
std::atomic适合两个都要写 强原子性
volatile适合一个读一个写
【c&&c++】volatile关键字的作用
FreeLikeTheWind.
于 2023-12-07 23:00:00 发布
阅读量1k
收藏 22
点赞数 17
分类专栏: c++ c语言 文章标签: c语言 c++ java
版权
c++
同时被 2 个专栏收录
67 篇文章0 订阅
订阅专栏
c语言
3 篇文章0 订阅
订阅专栏
volatile描述
volatile 是C和C++都支持的一个关键字,是一种类型修饰符。这个关键字被设计用来告诉编译器,一个变量可能会在程序之外被改变,例如,它可能被中断服务程序修改,或者它可能映射到一个硬件寄存器,这个寄存器的值可能由硬件改变。因此,编译器不应对涉及volatile变量的操作进行优化,因为这些优化可能会假设变量的值在两次访问之间不会改变。
需要注意的是,volatile并不能保证操作的原子性。在多线程环境中,如果一个volatile变量被同时修改,仍然可能会发生数据竞争。因此,在多线程编程中,std::atomic通常是一个更好的选择,因为它不仅防止了编译器的优化,还提供了原子性和内存一致性的保证。
volatile作用
以下是volatile的主要作用:
防止编译器优化:遇到volatile关键字声明的变量,编译器对访问该变量的代码不再进行优化,可以提供对特殊地址的稳定访问。
确保数据一致性:被volatile修饰的变量,系统每次用到它时,都是直接从对应的内存中提取,而不会利用缓存。这样就防止了多线程操作同一变量时,由于缓存导致的数据不一致性问题。
volatile数据操作示例
volatile示例,多个线程将会对同一个volatile变量进行操作:
#include <iostream>
#include <thread>
#include <atomic>
#include <chrono>
#include <vector>
// 全局的volatile变量
volatile int shared_data = 0;
// 一个线程将会执行的任务
void increment(int n) {
for (int i = 0; i < n; ++i) {
++shared_data;
// 休眠一段时间来模拟复杂操作
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
int main() {
const int num_threads = 5;
const int num_increments = 100;
std::vector<std::thread> threads;
// 创建并启动多个线程
for (int i = 0; i < num_threads; ++i) {
threads.push_back(std::thread(increment, num_increments));
}
// 等待所有线程完成
for (auto& thread : threads) {
thread.join();
}
// 输出volatile变量的值
std::cout << "shared_data: " << shared_data << std::endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
以上代码作用:
示例中,定义一个全局的volatile变量shared_data,多个线程将会同时对这个变量进行增加操作。
每个线程将会对shared_data进行num_increments次增加操作,每次增加操作后,线程将会休眠一段时间来模拟复杂的操作。
主线程将会等待所有子线程完成后,输出shared_data的值。
由于shared_data是volatile的,所以每个线程在读取它的值时,都会直接从内存中读取,而不是从自己的缓存中读取,这就保证了所有线程在任何时候看到的shared_data的值都是最新的。
输出结果变化原因及解决方案
如果每个线程都正确地增加了shared_data变量的值,那么最终的输出应该是500(5个线程,每个线程增加100次,总共增加500次)。然而,这个程序的输出可能每次都不同,这是因为多个线程可能同时对shared_data变量进行操作,导致数据竞争(data race)的问题。
但每次输出,结果不是500,每次都有变化,原因是:
数据竞争发生在至少一个线程正在写入一个内存位置,并且至少有一个其他线程正在读取或写入同一个内存位置,并且这两个操作中的至少一个是未同步的。在这种情况下,读取操作可能会读取到一个中间值,这个值是由两个写入操作的部分结果组成的。这就是为什么即使每个线程都正确地增加了shared_data的值,最终的输出可能仍然是不正确的。
在C++中,volatile关键字并不能保证操作的原子性。即使shared_data变量是volatile的,一个线程在读取它的值时可能会被另一个线程的写入操作打断,导致读取到一个中间值。这就是为什么volatile关键字不能保证在多线程环境中正确地同步数据。
要解决这个问题,可以使用C++11引入的std::atomic库。std::atomic库提供了一种在多线程环境中安全地操作数据的方法。
以下是使用std::atomic优化后的例子:
#include <iostream>
#include <thread>
#include <atomic>
#include <chrono>
#include <vector>
// 全局的std::atomic变量
std::atomic<int> shared_data(0);
// 线程将会执行的任务
void increment(int n) {
for (int i = 0; i < n; ++i) {
// 使用fetch_add方法原子地增加shared_data的值
shared_data.fetch_add(1);
// 休眠一段时间来模拟复杂操作
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
int main() {
const int num_threads = 5;
const int num_increments = 100;
std::vector<std::thread> threads;
// 创建并启动多个线程
for (int i = 0; i < num_threads; ++i) {
threads.push_back(std::thread(increment, num_increments));
}
// 等待所有线程完成
for (auto& thread : threads) {
thread.join();
}
// 输出std::atomic变量的值
std::cout << "shared_data: " << shared_data << std::endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
示例中,shared_data变量被声明为std::atomic,并且使用fetch_add方法原子地增加它的值。这样,即使有多个线程同时对shared_data进行操作,也不会发生数据竞争的问题,因为每个操作都是原子的。这将确保每次程序的输出都是500。
分析:volatile 和 std::atomic 的区别
std::atomic和volatile在内存模型上有一些不同。
首先,std::atomic是C++11中引入的,设计用来解决多线程数据竞争问题的工具。它提供了强类型的原子操作,包括load, store, exchange, compare_exchange_strong等,这些都是线程安全的。这意味着,当你在多线程环境下对一个std::atomic变量进行操作时,这些操作是不可中断的,即它们是原子的。因此,不会出现一个线程正在写入数据,而另一个线程读取到的是部分写入的数据这种情况。
至于std::atomic变量的读取操作是否直接从内存中读取数据,还是从线程的缓存中读取,这实际上取决于具体的实现和硬件架构。在大多数情况下,为了提高性能,现代处理器通常会使用缓存来存储最近访问的数据。当一个线程尝试读取一个std::atomic变量时,如果这个变量的值已经在该线程的缓存中,那么该线程可能会直接从缓存中读取这个值,而不是从内存中读取。然而,如果其他线程已经修改了这个变量的值,并且这个新的值还没有被当前线程缓存,那么当前线程将会从内存中读取这个新的值。这个过程是由硬件和操作系统自动管理的,对于程序员来说是透明的。
另一方面,volatile关键字告诉编译器不要优化涉及这个变量的操作,但并不保证操作的原子性。也就是说,如果一个volatile变量在多线程环境中被同时修改,仍然可能会发生数据竞争。而且,volatile并不能保证变量的值一定会从内存中读取,而不是从线程的缓存中读取。
通常来说,如果你已经使用了std::atomic,那么通常不需要再额外使用volatile。std::atomic已经提供了你需要的所有保证。
因为std::atomic已经提供了原子性和内存一致性的保证。原子性确保了操作是不可中断的,即它们要么完全执行,要么完全不执行。内存一致性保证了所有线程看到的变量值是一致的。这意味着,当一个线程修改了一个std::atomic变量的值,其他线程将会立即看到这个新的值,而不管它们是否有自己的缓存。
因此,通常建议只使用std::atomic来处理多线程环境中的共享变量。
volatile关键字用法示例
对寄存器进行赋值时,可以使用volatile关键字。volatile关键字告诉编译器,变量的值可能会在程序之外被改变,因此编译器不应对涉及该变量的操作进行优化。
在某些情况下,寄存器的值可能会被硬件或其他中断服务程序修改。如果在程序中访问这样的寄存器,并且希望每次访问都能得到最新的值,那么可以使用volatile关键字来声明该寄存器变量。
需要注意的是,volatile并不能保证操作的原子性。如果有多个线程或中断服务程序同时修改同一个寄存器变量,仍然可能会发生数据竞争。在这种情况下,需要使用其他同步机制,例如锁或原子操作,来确保操作的正确性和一致性。
下面使用volatile关键字对寄存器进行赋值示例:
#include <iostream>
#include <thread>
#include <chrono>
// 假设我们有一个硬件寄存器,它的地址是0x12345678
#define REGISTER_ADDRESS 0x12345678
// 声明一个volatile指针,指向该寄存器
volatile unsigned int* registerPtr = (volatile unsigned int*)REGISTER_ADDRESS;
int main() {
// 启动一个线程,不断读取寄存器的值并输出
std::thread readerThread([]{
while (true) {
unsigned int value = *registerPtr; // 读取寄存器的值
std::cout << "Register value: " << value << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1)); // 每隔1秒读取一次
}
});
// 主线程不断修改寄存器的值
unsigned int count = 0;
while (true) {
*registerPtr = count++; // 对寄存器进行赋值
std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 每隔500毫秒修改一次
}
// 等待读取线程结束(实际上这个程序不会正常结束,需要手动停止)
readerThread.join();
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
示例中,假设有一个硬件寄存器,它的地址是0x12345678。
之后声明了一个volatile unsigned int*类型的指针registerPtr,指向该寄存器的地址。
这样,通过对registerPtr进行解引用,可以读取或修改该寄存器的值。
在main函数中,启动了一个线程readerThread,它不断读取寄存器的值并输出到控制台。主线程则不断修改寄存器的值。由于registerPtr被声明为volatile,编译器不会对涉及该指针的操作进行优化,确保了每次读取和修改都能得到最新的值。
结论
万事开头难,然后中间难,最后结尾难。
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/MrHHHHHH/article/details/134867129