【c&&c++】volatile关键字的作用

​​volatile​​描述

​​volatile​​ 是C和C++都支持的一个关键字,是一种类型修饰符。这个关键字被设计用来告诉编译器,一个变量可能会在程序之外被改变,例如,它可能被中断服务程序修改,或者它可能映射到一个硬件寄存器,这个寄存器的值可能由硬件改变。因此,编译器不应对涉及volatile变量的操作进行优化,因为这些优化可能会假设变量的值在两次访问之间不会改变。

需要注意的是,volatile并不能保证操作的原子性。在多线程环境中,如果一个volatile变量被同时修改,仍然可能会发生数据竞争。因此,在多线程编程中,std::atomic通常是一个更好的选择,因为它不仅防止了编译器的优化,还提供了原子性和内存一致性的保证。

​​volatile​​作用

以下是volatile的主要作用:

  1. 防止编译器优化:遇到volatile关键字声明的变量,编译器对访问该变量的代码不再进行优化,可以提供对特殊地址的稳定访问。
  2. 确保数据一致性:被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;  
}

以上代码作用:
示例中,定义一个全局的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;
}

示例中,​​shared_data​​变量被声明为std::atomic,并且使用fetch_add方法原子地增加它的值。这样,即使有多个线程同时对​​shared_data​​进行操作,也不会发生数据竞争的问题,因为每个操作都是原子的。这将确保每次程序的输出都是500

分析:volatilestd::atomic 的区别

std::atomicvolatile在内存模型上有一些不同。

首先,std::atomicC++11中引入的,设计用来解决多线程数据竞争问题的工具。它提供了强类型的原子操作,包括load, store, exchange, compare_exchange_strong等,这些都是线程安全的。这意味着,当你在多线程环境下对一个std::atomic变量进行操作时,这些操作是不可中断的,即它们是原子的。因此,不会出现一个线程正在写入数据,而另一个线程读取到的是部分写入的数据这种情况。

至于std::atomic变量的读取操作是否直接从内存中读取数据,还是从线程的缓存中读取,这实际上取决于具体的实现和硬件架构。在大多数情况下,为了提高性能,现代处理器通常会使用缓存来存储最近访问的数据。当一个线程尝试读取一个std::atomic变量时,如果这个变量的值已经在该线程的缓存中,那么该线程可能会直接从缓存中读取这个值,而不是从内存中读取。然而,如果其他线程已经修改了这个变量的值,并且这个新的值还没有被当前线程缓存,那么当前线程将会从内存中读取这个新的值。这个过程是由硬件和操作系统自动管理的,对于程序员来说是透明的。

另一方面,volatile关键字告诉编译器不要优化涉及这个变量的操作,但并不保证操作的原子性。也就是说,如果一个volatile变量在多线程环境中被同时修改,仍然可能会发生数据竞争。而且,volatile并不能保证变量的值一定会从内存中读取,而不是从线程的缓存中读取。
通常来说,如果你已经使用了std::atomic,那么通常不需要再额外使用volatilestd::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;
}

示例中,假设有一个硬件寄存器,它的地址是​​0x12345678​​。

之后声明了一个volatile unsigned int*​​​类型的指针​​registerPtr​​,指向该寄存器的地址。

这样,通过对​​registerPtr​​进行解引用,可以读取或修改该寄存器的值。

​​main​​​函数中,启动了一个线程​​readerThread​​​,它不断读取寄存器的值并输出到控制台。主线程则不断修改寄存器的值。由于​​registerPtr​​​被声明为​​volatile​​,编译器不会对涉及该指针的操作进行优化,确保了每次读取和修改都能得到最新的值。

结论

万事开头难,然后中间难,最后结尾难

猜你喜欢

转载自blog.csdn.net/MrHHHHHH/article/details/134867129