您的位置:首页 > 新闻 > 会展 > android studio下载安装_河北邯郸魏县_软文推广策划方案_抖音seo优化排名

android studio下载安装_河北邯郸魏县_软文推广策划方案_抖音seo优化排名

2025/5/9 8:50:54 来源:https://blog.csdn.net/m0_52043808/article/details/144367962  浏览:    关键词:android studio下载安装_河北邯郸魏县_软文推广策划方案_抖音seo优化排名
android studio下载安装_河北邯郸魏县_软文推广策划方案_抖音seo优化排名

其他编程语言中(Java和C#),volatile是有并发含义的,即使在C++中,有些编译器在实现时也将并发的某种含义加入到了volatile关键字中。因此讨论volatile关键字的含义以消除异议。

atomic

std::atomic模板这种模板的实例化(比如,std::atomic<int>std::atomic<bool>std::atomic<Widget*>等)提供了一种在其他线程看来操作是原子性的的保证。一旦std::atomic对象被构建,在其上的操作表现得像操作是在互斥锁保护的关键区内,但是通常这些操作是使用特定的机器指令实现,这比锁的实现更高效。

分析如下使用std::atmoic的代码:

std::atomic<int> ai(0);       //初始化ai为0
ai = 10;                        //原子性地设置ai为10std::cout << ai;             //原子性地读取ai的值
++ai;                           //原子性地递增ai到11
--ai;                           //原子性地递减ai到10

在这些语句执行过程中,其他线程读取ai,只能读取到0,10,11三个值其中一个。

这个例子中有两点值得注意。首先,在“std::cout << ai;”中,ai是一个std::atomic的事实只保证了对ai的读取是原子的。没有保证整个语句的执行是原子的。在读取ai的时刻与调用operator<<将值写入到标准输出之间,另一个线程可能会修改ai的值。这对于这个语句没有影响,因为intoperator<<是使用int型的传值形参来输出(所以输出的值就是读取到的ai的值),但是重要的是要理解原子性的范围只保证了读取ai是原子性的。

ai的递增递减都是读-改-写(read-modify-writeRMW)操作,它们整体作为原子执行。std::atomic类型的最优的特性之一:一旦std::atomic对象被构建,所有成员函数,包括RMW操作,从其他线程来看都是原子性的。

相反,使用volatile在多线程中实际上不保证任何事情:

volatile int vi(0);          //初始化vi为0
vi = 10;                     //设置vi为10 std::cout << vi; //读vi的值
++vi;                        //递增vi到11
--vi;                        //递减vi到10

代码的执行过程中,如果其他线程读取vi,可能读到任何值,比如-12,68,409072,任何值。这份代码有未定义行为,因为这里的语句修改vi,所以如果同时其他线程读取vi,同时存在多个readers和writers读取没有std::atomic或者互斥锁保护的内存,这就是数据竞争的定义。

举一个关于在多线程程序中std::atomicvolatile表现不同的具体例子,考虑这样一个简单的计数器,通过多线程递增。把它们初始化为0:

std::atomic<int> ac(0);         
//“原子性的计数器 
volatile int vc(0);             
//“volatile计数器”

然后我们在两个同时运行的线程中对两个计数器递增:

/*----- Thread 1 ----- *//*------- Thread 2 ------- */++ac;                              ++ac;++vc;                              ++vc;

当两个线程执行结束时,ac的值(即std::atomic的值)肯定是2,因为每个自增操作都是不可分割的(原子性的)。另一方面,vc的值,不一定是2,因为自增不是原子性的。每个自增操作包括了读取vc的值,增加读取的值,然后将结果写回到vc。这三个操作对于volatile对象不能保证原子执行,所有可能是下面的交叉执行顺序:

  1. Thread1读取vc的值,是0。
  2. Thread2读取vc的值,还是0。
  3. Thread1将读到的0加1,然后写回到vc
  4. Thread2将读到的0加1,然后写回到vc

vc的最后结果是1,即使看起来自增了两次。

不仅只有这一种可能的结果,vc的最终结果是不可预测的,因为vc会发生数据竞争,对于数据竞争造成未定义行为,标准规定表示编译器生成的代码可能是任何逻辑。

RMW操作不是仅有的std::atomic在并发中有效而volatile无效的例子。假定一个任务计算第二个任务需要的一个重要的值。当第一个任务完成计算,必须传递给第二个任务。Item39表明一种使用std::atomic<bool>的方法来使第一个任务通知第二个任务计算完成。计算值的任务的代码如下:

std::atomic<bool> valVailable(false); 
auto imptValue = computeImportantValue();   //计算值
valAvailable = true;                        //告诉另一个任务,值可用了

人类读这份代码,能看到在valAvailable赋值之前对imptValue赋值很关键,但是所有编译器看到的是给相互独立的变量的一对赋值操作。通常来说,编译器会被允许重排这对没有关联的操作。这意味着,给定如下顺序的赋值操作(其中abxy都是互相独立的变量),

a = b;
x = y;

编译器可能重排为如下顺序:

x = y;
a = b;

即使编译器没有重排顺序,底层硬件也可能重排(或者可能使它看起来运行在其他核心上),因为有时这样代码执行更快。std::atomic会限制这种重排序,并且这样的限制之一是,在源代码中,对std::atomic变量写之前不会有任何操作。(这只在std::atomic使用顺序一致性时成立,对于使用在本书中展示的语法的std::atomic对象,这也是默认的和唯一的一致性模型。C++11也支持带有更灵活的代码重排规则的一致性模型。这样的弱(weak)模型使构建一些软件在某些硬件构架上运行的更快成为可能,但是使用这样的模型产生的软件更加难改正、理解、维护。在使用松散原子性的代码中微小的错误很常见,即使专家也会出错,所以应当尽可能坚持顺序一致性。)

auto imptValue = computeImportantValue(); //计算值
valAvailable = true;                      //告诉另一个任务,值可用了

编译器不仅要保证imptValuevalAvailable的赋值顺序,还要保证生成的硬件代码不会改变这个顺序。结果就是,将valAvailable声明为std::atomic确保了必要的顺序——其他线程看到的是imptValue值的改变不会晚于valAvailable。将valAvailable声明为volatile不能保证上述顺序:

volatile bool valVailable(false); 
auto imptValue = computeImportantValue();
val Available = true;                        
//其他线程可能看到这个赋值操作早于imptValue的赋值操作

这份代码编译器可能将imptValuevalAvailable赋值顺序对调,如果它们没这么做,可能不能生成机器代码,来阻止底部硬件在其他核心上的代码看到valAvailable更改在imptValue之前。

不保证操作的原子性以及对代码重排顺序没有足够限制,解释了为什么volatile在多线程编程中没用,但是没有解释它应该用在哪。它是用来告诉编译器,它们处理的内存有不正常的表现。“正常”内存应该有这个特性,在写入值之后,这个值会一直保持直到被覆盖。假设有这样一个正常的int

int x;

编译器看到下列的操作序列:

auto y = x;                             //读x
y = x;                                  //再次读x

编译器可通过忽略对y的一次赋值来优化代码,因为有了y初始化,赋值是冗余的。

正常内存还有一个特征,就是如果你写入内存没有读取,再次写入,第一次写就可以被忽略,因为值没被用过。给出下面的代码:

x = 10;                                 //写x
x = 20;                                 //再次写x

编译器可以忽略第一次写入。这意味着如果写在一起:

auto y = x;                             //读x
y = x;                                  //再次读x
x = 10;                                 //写x
x = 20;                                 //再次写x

编译器生成的代码是这样的:

auto y = x;                             //读x
x = 20;                                 //写x

可能你会想谁会写这种重复读写的代码(技术上称为冗余访问redundant loads)和无用存储dead stores)),答案是开发者不会直接写——至少我们不希望开发者这样写。但是在编译器拿到看起来合理的代码,执行了模板实例化,内联和一系列重排序优化之后,结果会出现冗余访问和无用存储,所以编译器需要摆脱这样的情况并不少见。

这种优化仅仅在内存表现正常时有效,特殊的内存不行。最常见的“特殊”内存是用来做内存映射I/O的内存。这种内存实际上是与外围设备(比如传感器,显示器,打印机,网络端口)通信,而不是读写通常的内存(比如RAM)。这种情况下,再次考虑这看起来冗余的代码:

auto y = x;                             //读x
y = x;                                  //再次读x

如果x的值是一个温度传感器上报的,第二次对于x的读取就不是多余的,因为温度可能在第一次和第二次读取之间变化。看起来冗余的写操作也类似。比如在这段代码中:

x = 10;                                 //写x
x = 20;                                 //再次写x

如果x与无线电发射器的控制端口关联,则代码是给无线电发指令,10和20意味着不同的指令。优化掉第一条赋值会改变发送到无线电的指令流。

volatile是告诉编译器我们正在处理特殊内存。意味告诉编译器“不要对这块内存执行任何优化”。所以如果x对应于特殊内存,应该声明为volatile

volatile int x;

考虑对我们的原始代码序列有何影响:

auto y = x;                             //读x
y = x;                                  //再次读x(不会被优化掉)
x = 10;                                 //写x(不会被优化掉)
x = 20;                                 //再次写x

如果x是内存映射的(或者已经映射到跨进程共享的内存位置等),这正是我们想要的。

在最后一段代码中,y是什么类型:int还是volatile int?(y的类型使用auto类型推导,所以使用Item2中的规则。规则上说非引用非指针类型的声明(就是y的情况),constvolatile限定符被拿掉。y的类型因此仅仅是int。这意味着对y的冗余读取和写入可以被消除。在例子中,编译器必须执行对y的初始化和赋值两个语句,因为xvolatile的,所以第二次对x的读取可能会产生一个与第一次不同的值)

在处理特殊内存时,必须保留看似冗余访问和无用存储的事实,顺便说明了为什么std::atomic不适合这种场景。编译器被允许消除对std::atomic的冗余操作。代码的编写方式与volatile那些不那么相同,但是如果我们暂时忽略它,只关注编译器执行的操作,则概念上可以说,编译器看到这个,

Volatile

volatile关键字用于告诉编译器某个变量可能会被外部因素(如硬件中断或并发线程)改变,因此每次访问该变量时都必须实际读取或写入内存,而不是从寄存器或缓存中获取值。这防止了编译器优化掉看似冗余的读写操作,确保了对特殊内存位置(例如I/O端口、硬件寄存器等)的正确处理。

volatile int x;
auto y = x; // 读x
y = x;      // 再次读x,不会被优化掉
x = 10;     // 写x,不会被优化掉
x = 20;     // 再次写x

由于auto推导出的y是一个普通的int类型,它不受volatile的影响,因此对y的操作可以被优化。但是,因为x是volatile的,所以所有对x的访问都不能被优化。

然而,std::atomic并不阻止编译器进行某些类型的优化,比如合并连续的读操作。此外,std::atomic不允许拷贝构造或赋值,以防止无法实现真正的原子性操作。因此,要与普通变量交互,需要使用load()和store()方法:

std::atomic<int> x;
int y = x.load();   // 读x
x.store(10);        // 写x

结合使用 volatile 和 std::atomic

对于既需要原子性又需要防止编译器优化的情况(例如访问共享内存中的硬件寄存器),可以将两者结合起来使用:

volatile std::atomic<int> vai; // 对vai的操作是原子性的,且不能被优化

这样定义的变量既可以保证线程安全,也可以确保所有访问都不会被编译器优化掉。

因此情况很明显:

  • std::atomic用在并发编程中,对访问特殊内存没用。
  • volatile用于访问特殊内存,对并发编程没用。

因为std::atomicvolatile用于不同的目的,所以可以结合起来使用:

volatilestd::atomic<int> vai;          //对vai的操作是原子性的,且不能被优化掉

请记住:

  • std::atomic用于在不使用互斥锁情况下,来使变量被多个线程访问的情况。是用来编写并发程序的一个工具。
  • volatile用在读取和写入不应被优化掉的内存上。是用来处理特殊内存的一个工具。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com