1:const+volatile解决内存可见性问题,2:共享数据的访问导致竞争条件(Race Condition)

1:const+volatile解决内存可见性问题

两种const修饰指针的方式:

  1. 指针指向的内容是常量

    复制代码
    const int* p;  // 等价于 int const* p;
    • 含义p是一个指针,它指向的那个int型数据是常量 ,不能通过p来修改。

    • 记忆const没有紧贴p,修饰的是指向的数据

  2. 指针本身是常量

    复制代码
    int* const p = &some_int_var; // 必须初始化!
    • 含义p本身是一个常量指针 ,即指针的指向(它保存的地址)不能再改变。但可以通过p去修改它指向的那个数据。

    • 记忆 :const紧贴p,修饰的是指针变量本身

  3. 双保险(指针和指向的数据都是常量)

    复制代码
    const int* const p = &some_int_const_var;
    • 含义p的指向不能变,通过p也不能修改其指向的值。

在硬件寄存器访问中的应用

在嵌入式系统中,我们经常需要读写内存映射的I/O寄存器 。这些寄存器是物理硬件的窗口,它们的访问有特殊要求,const的正确使用是保障安全的关键。

场景一:只读寄存器(如状态寄存器) -> 使用 const int*

状态寄存器通常由硬件改变,软件只能读取其值,任何写入操作都可能导致未定义行为 。为了确保程序员不会误写,必须用const保护。

复制代码
// 假设 STATUS_REG 是一个内存映射的只读寄存器地址
#define STATUS_REG  ((volatile const uint32_t*)0x40021000)

void read_device_status() {
    volatile const uint32_t* status_ptr = STATUS_REG; // 指针指向的内容是const
    uint32_t current_status = *status_ptr; // 正确:读取
    // *status_ptr = 0x00; // 错误!编译器会报错:向只读位置赋值
    // 这从语法层面防止了我们对只读寄存器的误写,这是最重要的安全保护。
}

关键点

  • volatile告诉编译器这个值可能被硬件意外改变,禁止做优化(如把读操作缓存到寄存器)。

  • const保护数据不被软件意外修改,是语义安全的关键。

场景二:固定地址的配置寄存器 -> 使用 int* const

有时,我们对一个特定的、地址固定的寄存器进行连续的配置操作。指针的指向(即这个固定地址)不应该改变,但我们需要修改其指向的值。

复制代码
// 配置一个特定的控制寄存器
void configure_uart_baudrate() {
    volatile uint32_t* const ctrl_reg = (volatile uint32_t*)0x40013800; // 指针本身是const
    *ctrl_reg = 0x0001; // 正确:写入配置值A
    *ctrl_reg |= 0x0008; // 正确:再次写入,修改某些位
    // ctrl_reg = (volatile uint32_t*)0x40013000; // 错误!指针的指向不能变。
    // 这确保了我们的操作对象始终是目标寄存器,避免了指针被意外指向别处。
}

关键点

  • 这里的const保护了指针变量本身,防止编程时因指针被意外修改而写到错误的内存地址,导致系统崩溃。这在复杂的驱动初始化函数中能避免许多难以调试的错误。

场景三:指向固定只读寄存器 -> 使用 const int* const

这结合了以上两者,是最严格的保护,常用于定义那些地址固定、且只读的硬件资源。

复制代码
// 一个只读的设备ID寄存器
volatile const uint32_t* const DEVICE_ID_REG = (volatile const uint32_t*)0x40022100;
// 1. volatile: 值可能硬件改变
// 2. 第一个const: 指向的32位ID是只读的
// 3. 第二个const: 指针本身指向的地址0x40022100是固定的

uint32_t get_chip_id() {
    return *DEVICE_ID_REG; // 只能读,且指针地址固定
    // 任何写入或改变指针指向的操作都会被编译器阻止。
}

2:共享数据的访问导致竞争条件(Race Condition)

竞争条件

竞争条件 是多线程编程中的一个经典并发问题。其核心定义是:当多个线程(或进程)在没有适当同步机制的情况下,并发访问和操作同一个共享数据(如变量、内存、文件、设备等),且最终的执行结果依赖于这些线程执行指令的精确先后顺序时,就发生了竞争条件。

一个简单的类比:

想象一个共享的在线文档(共享数据),上面记录着数字"5"(初始值)。

  • 线程A的任务是读取这个数字,然后将其加1,并写回结果。

  • 线程B的任务是读取这个数字,然后将其乘以2,并写回结果。

如果没有同步(即发生了竞争条件),可能的结果会因执行顺序而变得不确定:

可能的执行顺序 线程A操作 线程B操作 最终结果 解释
**理想情况(顺序执行)**​ 读(5)-> 加1 -> 写(6) 读(6) -> 乘2 -> 写(12) 12 结果确定且符合预期。
**竞争条件(交错执行)**​ 读(5) 读(5) 两线程都读到初始值5。
加1 -> 写(6) 乘2 -> 写(10) 10 A的结果被B覆盖。
**竞争条件(另一种交错)**​ 读(5) 读(5) 两线程都读到初始值5。
乘2 -> 写(10) 6 B的结果被A覆盖。
加1 -> 写(6)

具体体现和危害:

您在项目中使用OpenMP对循环进行并行化改造。假设循环体中有一个累加操作 sum += data[i];,如果多个线程同时执行这条语句,就会发生竞争条件,因为 sum是一个共享变量,+=操作本身是"读-改-写"三个步骤的组合,不是原子操作。最终计算出的 sum值很可能是错误的,因为它取决于线程执行的随机交错顺序。

解决方案

OpenMP的 reduction子句 是OpenMP为解决此类问题提供的一种内置同步机制。reduction(+:sum)会告诉编译器,为每个线程创建一个 sum的私有副本,线程在自己的私有副本上进行累加,在所有线程计算结束后,再自动将所有私有副本的值按指定的操作符(这里是加法+)合并到全局的 sum变量中。这就彻底消除了 对共享变量 sum的竞争访问,保证了结果的正确性。

相关推荐
cccyi72 分钟前
【C++ 脚手架】gtest 单元测试库的介绍与使用
c++·单元测试·gtest
2501_941982052 分钟前
Go 语言实现企业微信外部群消息主动推送方案
开发语言·golang·企业微信
南山love3 分钟前
spring-boot多线程并发执行任务
java·开发语言
weixin_436182425 分钟前
物联网端 - 边 - 云协同架构:头部厂商完整平台甄选方法
物联网·架构
一叶飘零_sweeeet6 分钟前
消息队列选型终极指南:Kafka、RocketMQ、RabbitMQ 底层原理与场景化选型全解
架构·kafka·rabbitmq·rocketmq·消息队列选型
dmlcq7 分钟前
一文读懂 PageQueryUtil:分页查询的优雅打开方式
开发语言·windows·python
不会写DN7 分钟前
JS 最常用的性能优化 防抖和节流
开发语言·javascript·ecmascript
悲伤小伞9 分钟前
10-MySQL_事务管理
linux·数据库·c++·mysql·centos
HLC++10 分钟前
数据结构--树
c语言·开发语言·数据结构
2501_9454248011 分钟前
C++构建缓存加速
开发语言·c++·算法