首页 > 其他分享 >[c语言]volatile关键字的作用

[c语言]volatile关键字的作用

时间:2023-12-07 12:04:18浏览次数:33  
标签:std 语言 int 关键字 线程 atomic volatile 变量

volatile描述

volatileCC++都支持的一个关键字,是一种类型修饰符。这个关键字被设计用来告诉编译器,一个变量可能会在程序之外被改变,例如,它可能被中断服务程序修改,或者它可能映射到一个硬件寄存器,这个寄存器的值可能由硬件改变。因此,编译器不应对涉及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;  
}
#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。

分析:volatile 和 std::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并不能保证操作的原子性。如果有多个线程或中断服务程序同时修改同一个寄存器变量,仍然可能会发生数据竞争。在这种情况下,需要使用其他同步机制,例如锁或原子操作,来确保操作的正确性和一致性。

好的,下面是一个以C++格式举例说明使用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,编译器不会对涉及该指针的操作进行优化,确保了每次读取和修改都能得到最新的值。

结论

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

标签:std,语言,int,关键字,线程,atomic,volatile,变量
From: https://blog.51cto.com/u_16417016/8719774

相关文章

  • 集合异或运算--记录学习C语言每一天
    ////main.c//Hello////Createdbyrenxinon2023/11/28.//#defineElemTypeint#defineMaxSize50#include<stdio.h>#include<stdlib.h>typedefstructList{ElemTypeList[MaxSize];intLength;intSize;}List;voidIn......
  • vscode-go语言插件,调试器协议分析
    c客户端,vscodes服务端,调试器----------------------------------------------c-->客户端,请求调试器初始化{"command":"initialize","arguments":{"clientID":"vscode","clientName":......
  • 使用预训练语言模型作帖子分类
    ​​ 预训练语言模型PLMs或PTMs应用广泛且效果良好。有的文章中把自然语言处理中的预训练语言模型的发展划分为4个时代:词入时代,上下文嵌入(ContextWordEmbedding)时代、预训练语言模型时代、改进型和领域定制型时代。为什么需要预训练​ 模型通常需要非常大的参数量,但并不是......
  • 【视频】Copula算法原理和R语言股市收益率相依性可视化分析|附代码数据
    阅读全文:http://tecdat.cn/?p=6193最近我们被客户要求撰写关于Copula的研究报告,包括一些图形和统计输出。copula是将多变量分布函数与其边缘分布函数耦合的函数,通常称为边缘。在本视频中,我们通过可视化的方式直观地介绍了Copula函数,并通过R软件应用于金融时间序列数据来理解它......
  • R语言SIR模型网络结构扩散过程模拟SIR模型(Susceptible Infected Recovered )代码实例|
    全文链接:http://tecdat.cn/?p=14593最近我们被客户要求撰写关于SIR模型的研究报告,包括一些图形和统计输出。与普通的扩散研究不同,网络扩散开始考虑网络结构对于扩散过程的影响。这里介绍一个使用R模拟网络扩散的例子基本的算法非常简单:生成一个网络:g(V,E)。随机选择一个或几......
  • C语言进阶教程(include只能包含.h文件吗?)
    (文章目录)前言include在多文件编程中是非常重要的,我们经常使用他来包含一些头文件,方便我们管理代码和项目,那么include是只能包含头文件吗?这篇文章将会告诉大家include是不是只能包含头文件。一、include工作原理在C语言中,#include是预处理指令,它告诉编译器在源代码中包含另......
  • 探索C语言中的Shellcode从提取到执行
    ShellCode是一种独立于应用程序的机器代码,通常用于实现特定任务,如执行远程命令、注入恶意软件或利用系统漏洞。在网络安全领域,研究Shellcode是理解恶意软件和提高系统安全性的关键一环。本文将深入探讨如何在C语言中提取Shellcode,并通过XOR加密技术增加其混淆程度。最后,我们将演示......
  • java中的关键字transient,将不需要序列化的属性前添加关键字transient,序列化对象的时候
    java中的关键字transient,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会被序列化这个关键字的作用其实我在写java的序列化机制中曾经写过,不过那时候只是简单地认识,只要其简单的用法,没有深入的去分析。这篇文章就是去深入分析一下transient关键字。先......
  • C语言三维智能PACS系统源码,医学影像采集系统
    三维智能PACS系统源码,医学影像采集传输系统源码PACS系统以大型关系型数据库作为数据和图像的存储管理工具,以医疗影像的采集、传输、存储和诊断为核心,集影像采集传输与存储管理、影像诊断查询与报告管理、综合信息管理等综合应用于一体的综合应用系统。日常产生的各种医学影像通过国......
  • 【转】编译型与解释型、动态语言与静态语言、强类型语言与弱类型语言的区别
    编译型和解释型我们先看看编译型,其实它和汇编语言是一样的:也是有一个负责翻译的程序来对我们的源代码进行转换,生成相对应的可执行代码。这个过程说得专业一点,就称为编译(Compile),而负责编译的程序自然就称为编译器(Compiler)。如果我们写的程序代码都包含在一个源文件中,那么通常编译......