[c&&c++] The role of volatile keyword

​​volatile​​Description

​​volatile​​ is a keyword supported by both C and C++ and is a type modifier. This keyword is designed to tell the compiler that a variable may be changed outside the program, for example, it may be modified by an interrupt service routine, or it may be mapped to a hardware register whose value may be changed by the hardware. Therefore, compilers should not optimize operations involving volatile variables because these optimizations may assume that the value of the variable does not change between accesses.

It should be noted that volatile does not guarantee the atomicity of the operation. In a multi-threaded environment, a data race may still occur if avolatile variable is modified simultaneously. Therefore, in multi-threaded programming, std::atomic is usually a better choice because it not only prevents compiler optimizations but also provides guarantees of atomicity and memory consistency.

​​volatile​​Function

The following are the main functions ofvolatile:

  1. Prevent compiler optimization: When encountering a variable declared with thevolatile keyword, the compiler will no longer optimize the code that accesses the variable, and can provide stable access to special addresses.
  2. Ensure data consistency: Every time the system uses a variable modified by volatile, it will be fetched directly from the corresponding memory without using the cache. This prevents data inconsistency problems caused by caching when multiple threads operate the same variable.

​​volatileData manipulation examples

volatileExample, multiple threads will operate on the samevolatile variable:

#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;  
}

The above code functions:
In the example, define a global volatile variable​​shared_data​​, multiple threads This variable will be incremented at the same time.

Each thread will perform addition operations on ​​shared_data​​​. After each addition operation, the thread will sleep for a period of time to simulate complex operate. The main thread will wait for all child threads to complete and then output the value of . ​​num_increments​​
​​shared_data​​

Since​​shared_data​​​isvolatile, so when each thread reads its value, it will read it directly from the memory. Instead of reading from its own cache, this ensures that the value of shared_data seen by all threads at any time is the latest.

Causes and solutions for changes in output results

If each thread correctly increments the value of the ​​shared_data​​ variable, the final output should be 500 (5 threads, each Added 100 times, a total of 500 times added). However, the output of this program may be different every time because multiple threads may operate on the variables at the same time, leading to data race problems. . shared_data​​(data race)

But every time it is output, the result is not500, it changes every time. The reason is:
Data competition occurs when at least one thread is writing A memory location, and at least one other thread is reading from or writing to the same memory location, and at least one of the two operations is unsynchronized. In this case, the read operation may read an intermediate value that is composed of the partial results of the two write operations. This is why even if each thread correctly increments the value of ​​shared_data​, the final output may still be incorrect.

In C++, the volatile keyword does not guarantee atomicity of the operation. Even if the shared_data​​ variable is volatile, a thread reading its value may be interrupted by another thread's writing operation, causing the read to an intermediate value. This is why thevolatile keyword is not guaranteed to synchronize data correctly in a multi-threaded environment.
To solve this problem, you can use the library introduced by C++11. The library provides a way to safely manipulate data in a multi-threaded environment. std::atomicstd::atomic

The following is an example optimized usingstd::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;
}

In the example, the ​​shared_data​​ variable is declared as std::atomic and its value is atomically increased using the fetch_add method. In this way, even if multiple threads operate on ​​shared_data​​ at the same time, data race problems will not occur because each operation is atomic. This will ensure that the output of the program is 500 every time.

Analysis:volatile Sum std::atomic Partition

std::atomic and volatile have some differences in memory models.

First of all, std::atomic is a tool introduced in C++11, designed to solve multi-threaded data race problems. It provides strongly typed atomic operations, including load, store, exchange, compare_exchange_strong, etc. , these are all thread-safe. This means that when you operate on a std::atomic variable in a multi-threaded environment, these operations are uninterruptible, that is, they are atomic. Therefore, there is no situation where one thread is writing data and another thread reads partially written data.

As forstd::atomicwhether the read operation of the variable reads the data directly from the memory or reads from the cache of the thread, this actually depends on the specific implementation and hardware architecture. In most cases, to improve performance, modern processors typically use caches to store recently accessed data. When a thread attempts to read a std::atomic variable, if the value of the variable is already in the thread's cache, the thread may read the value directly from the cache instead of from memory. read in. However, if another thread has modified the value of this variable, and the new value has not been cached by the current thread, then the current thread will read the new value from memory. This process is managed automatically by the hardware and operating system and is transparent to the programmer.

On the other hand, the volatile keyword tells the compiler not to optimize operations involving this variable, but does not guarantee the atomicity of the operation. That is, if a volatile variable is modified simultaneously in a multi-threaded environment, a data race may still occur. Moreover, volatile does not guarantee that the value of the variable will be read from memory rather than from the thread's cache.
Generally speaking, if you are already using std::atomic, then there is usually no need to use additional volatile. std::atomicAll the guarantees you need have been provided.

Because std::atomic already provides atomicity and memory consistency guarantees. Atomicity ensures that operations are uninterruptible, i.e. they either execute completely or not at all. Memory consistency ensures that the variable values ​​seen by all threads are consistent. This means that when one thread modifies the value of an std::atomic variable, other threads will immediately see the new value, regardless of whether they have their own cache.

Therefore, it is generally recommended to only usestd::atomic to handle shared variables in a multi-threaded environment.

volatileKeyword usage examples

When assigning a value to a register, you can use the volatile keyword. The volatile keyword tells the compiler that the value of the variable may be changed outside the program, so the compiler should not optimize operations involving the variable.

In some cases, the register value may be modified by hardware or other interrupt service routines. If you access such a register in a program and want to get the latest value every time you access it, you can use the volatile keyword to declare the register variable.

It should be noted that volatile does not guarantee the atomicity of the operation. If multiple threads or interrupt service routines modify the same register variable at the same time, a data race may still occur. In this case, other synchronization mechanisms, such as locks or atomic operations, need to be used to ensure correctness and consistency of operations.

The following is an example of using the volatile keyword to assign a value to a register:

#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;
}

In the example, assume there is a hardware register whose address is​​0x12345678​​.

After , a pointer of type is declared, pointing to the address of the register. volatile unsigned int*​​​​​registerPtr​​

In this way, by dereferencing​​registerPtr​​, the value of this register can be read or modified.

In the​​main​​​ function, a thread is started​​readerThread​​​, which continuously reads the value of the register and outputs it to the console. The main thread continuously modifies the register value. Since ​​registerPtr​​​ is declared as ​​volatile​​, the compiler will not optimize operations involving this pointer, ensuring that every read and modification You can get the latest value.

in conclusion

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

Guess you like

Origin blog.csdn.net/MrHHHHHH/article/details/134867129