2026/3/28 16:15:43
网站建设
项目流程
网站建设培训学校北京,推广什么,优化型网站建设,制作公司网站大概多少钱写C/C多线程程序#xff0c;绕不开线程安全问题。很多程序员看到共享变量#xff0c;第一反应是加个volatile关键字#xff0c;以为这样就能保证线程安全了。
但这是错的。
volatile在多线程中根本不够用#xff0c;它只能防止编译器优化#xff0c;不能保证原子性…写C/C多线程程序绕不开线程安全问题。很多程序员看到共享变量第一反应是加个volatile关键字以为这样就能保证线程安全了。但这是错的。volatile在多线程中根本不够用它只能防止编译器优化不能保证原子性也不能保证内存序用错了程序看起来没问题实际上随时可能崩而且这种Bug特别难复现往往在生产环境高并发时才暴露。今天聊聊3个最常见的volatile使用错误。错误1用volatile做计数器这是最常见的错误。很多人以为给计数器加个volatile就能在多线程中安全使用。看这段代码volatileintcounter0;voidthread_func(){for(inti0;i100000;i){counter;// 看起来没问题}}看起来没问题对吧但这段代码在多线程环境下会出错。为什么因为counter不是原子操作它实际上是三步从内存读取counter的值到寄存器寄存器的值加1把寄存器的值写回内存而volatile只能保证每次都从内存读取、每次都写回内存、不会被编译器优化掉但它不能保证这三步是原子的两个线程可能同时读取、同时加1、同时写回导致丢失更新。两个线程同时执行counter可能发生这种情况线程A读取counter0 线程B读取counter0 线程A计算011 线程B计算011 线程A写回counter1 线程B写回counter1两次加1结果counter只增加了1这就是数据竞争data race在高并发场景下这种问题会导致计数严重不准比如10000次加1操作最后counter可能只有8000多丢失了近2000次更新。正确做法用std::atomicstd::atomicintcounter(0);voidthread_func(){for(inti0;i100000;i){counter;// 原子操作线程安全}}std::atomic保证了原子性counter会被编译成一条原子指令比如x86的lock add整个操作不可分割不会被其他线程打断CPU通过缓存一致性协议保证这条指令执行期间的原子性从根本上杜绝了数据竞争。错误2用volatile保护共享数据结构有些程序员以为volatile能保护复杂的数据结构。比如这样structSharedData{intx;inty;intz;};volatileSharedData data;voidthread1(){data.x1;data.y2;data.z3;}voidthread2(){if(data.x1data.y2data.z3){// 做点什么}}这段代码有两个致命问题。问题1volatile不保证原子性thread1的三次赋值不是原子的thread2可能看到x1, y0, z0这种中间状态volatile不能把多个操作打包成原子操作每个赋值都是独立的线程切换可能发生在任何时候。问题2volatile不保证内存序更要命的是编译器和CPU可能会重排指令thread1的三次赋值顺序可能变成先写z、再写x、最后写yvolatile不提供happens-before保证它只保证每次访问都从内存读写但不保证访问的顺序thread2可能看到z3, x0, y0然后判断失败但实际上thread1已经执行完了只是顺序乱了。正确做法用mutex或atomic如果是简单的标志位用atomicstd::atomicboolready(false);voidthread1(){// 准备数据data.x1;data.y2;data.z3;ready.store(true,std::memory_order_release);// 保证前面的写操作都完成}voidthread2(){if(ready.load(std::memory_order_acquire)){// 保证能看到前面的写操作// 安全使用data}}memory_order_release和memory_order_acquire配对使用保证了内存序thread2看到readytrue时一定能看到thread1对data的所有修改这是因为release语义保证了之前的所有写操作都完成acquire语义保证了之后的所有读操作都能看到形成了一个同步点。如果是复杂的数据结构用mutexstd::mutex mtx;SharedData data;voidthread1(){std::lock_guardstd::mutexlock(mtx);data.x1;data.y2;data.z3;}voidthread2(){std::lock_guardstd::mutexlock(mtx);if(data.x1data.y2data.z3){// 做点什么}}mutex保证了互斥访问同一时刻只有一个线程能访问data不会出现中间状态虽然性能比atomic稍差因为涉及锁的获取释放开销以及在高竞争场景下可能的内核态切换和上下文切换但对于复杂数据结构来说这是最简单、最可靠的方案。错误3以为volatile能防止指令重排很多人以为volatile能防止指令重排序。这是对volatile最大的误解。看这个经典的双重检查锁定Double-Checked LockingvolatileSingleton*instancenullptr;Singleton*getInstance(){if(instancenullptr){// 第一次检查lock();if(instancenullptr){// 第二次检查instancenewSingleton();// 问题在这里}unlock();}returninstance;}这段代码看起来很聪明用volatile保证instance的可见性用双重检查减少锁的开销但它是错的。问题出在new Singleton()这个操作实际上是三步分配内存、在内存上构造Singleton对象、把内存地址赋值给instance而编译器和CPU可能会重排成分配内存、把内存地址赋值给instance此时对象还没构造完、在内存上构造Singleton对象如果线程A执行到第2步instance已经不是nullptr了但对象还没构造完这时线程B进来第一次检查发现instance不是nullptr直接返回了一个未构造完的对象程序崩溃。volatile不能防止这种重排。它只保证对volatile变量的访问不会被优化掉但不保证访问的顺序。正确做法用atomic memory_orderstd::atomicSingleton*instance(nullptr);Singleton*getInstance(){Singleton*tmpinstance.load(std::memory_order_acquire);if(tmpnullptr){lock();tmpinstance.load(std::memory_order_acquire);if(tmpnullptr){tmpnewSingleton();instance.store(tmp,std::memory_order_release);}unlock();}returntmp;}memory_order_release保证了new Singleton()的所有操作分配内存、构造对象都完成后才把地址写入instancememory_order_acquire保证了读取instance时能看到对象的完整状态锁内的第二次检查也使用acquire确保如果另一个线程已经创建了实例当前线程能看到完全构造好的对象这两个内存序配合使用形成了一个完整的同步机制彻底解决了双重检查锁定的问题。或者更简单用C11的局部静态变量SingletongetInstance(){staticSingleton instance;// C11保证线程安全returninstance;}C11标准保证了局部静态变量的初始化是线程安全的。编译器会自动加锁保证只初始化一次。那volatile到底该怎么用说了这么多volatile不能做的事那它到底能做什么volatile的设计初衷是处理内存映射I/O和信号处理。场景1硬件寄存器volatileuint32_t*gpio_register(uint32_t*)0x40020000;*gpio_register0x01;// 写入硬件寄存器硬件寄存器的值可能随时变化比如GPIO输入编译器不能假设它的值不变volatile告诉编译器每次都从这个地址读取不要优化因为硬件可能在任何时候修改这个值编译器的优化假设“这个变量我刚读过值不会变”在这里不成立。场景2信号处理函数volatilesig_atomic_t flag0;voidsignal_handler(intsig){flag1;}intmain(){signal(SIGINT,signal_handler);while(flag0){// 等待信号}}信号处理函数可能在任何时候被调用修改flag的值volatile保证编译器不会把while (flag 0)优化成死循环因为编译器可能认为flag在循环里没被修改可以优化成if (flag 0) { while(1); }“但信号处理函数是异步的编译器看不到这个修改volatile就是告诉编译器这个变量可能被外部修改别优化”。但注意这两个场景都是单线程的。在多线程中volatile不够用。你需要atomic、mutex、或者其他同步原语。总结volatile在多线程中的三个致命缺陷不保证原子性- 多步操作可能被打断不保证内存序- 指令可能被重排不提供同步- 没有happens-before保证记住一句话volatile是给编译器看的不是给CPU看的。它只能防止编译器优化不能防止CPU重排也不能保证原子性。多线程编程该用atomic就用atomic该用mutex就用mutex。别指望volatile能解决线程安全问题。它真正的用途是硬件寄存器和信号处理。仅此而已。