哪个场景关键字" volatile"是否需要在objective-c中声明?

时间:2015-07-21 09:56:22

标签: ios objective-c keyword

据我所知,volatile通常用于防止在某些硬件操作期间出现意外的编译优化。但是应该在属性定义中声明哪些场景volatile让我感到困惑。请举几个有代表性的例子。

THX。

3 个答案:

答案 0 :(得分:43)

编译器假定变量可以改变其值的唯一方法是通过改变它的代码。

int a = 24;

现在,编译器假定a24,直到它看到任何更改a值的语句。如果您在上面的语句

下面的某处编写代码
int b = a + 3;

编译器会说" 我知道a是什么,它是24!因此b27。我不必编写代码来执行该计算,我知道它将始终27 "。编译器可能只是优化整个计算。

但是如果a在赋值和计算之间发生了变化,编译器就会出错。但是,a为什么这样做?为什么a会突然出现不同的值?它赢了。

如果a是堆栈变量,则它不能更改值,除非您传递对它的引用,例如

doSomething(&a);

函数doSomething有一个指向a的指针,这意味着它可以更改a的值,在该行代码后,a可能不是{{} 1}}任何更长的时间。所以,如果你写

24

编译器不会优化计算。谁知道int a = 24; doSomething(&a); int b = a + 3; 之后a会有什么价值?编译器肯定没有。

使用全局变量或对象的实例变量,事情变得更加棘手。这些变量不在堆栈上,它们在堆上,这意味着不同的线程可以访问它们。

doSomething

// Global Scope int a = 0; void function ( ) { a = 24; b = a + 3; } b吗?很可能答案是肯定的,但是其他一些线程很可能在这两行代码之间改变了27的值,然后它就不会是a。编译器在乎吗?没有为什么?因为C对线程一无所知 - 至少它没有用过(最新的C标准最终知道本机线程,但之前的所有线程功能只是操作系统提供的API而不是本机的C)。因此,C编译器仍会假设27b并优化计算,这可能会导致错误的结果。

这是27有用的东西。如果你像那样标记变量volatile

volatile

您基本上是在告诉编译器:" volatile int a = 0; 的值可能随时发生变化。不认真,它可能会发生变化。你不会看到它来* *爆炸*,它有不同的价值!"。对于编译器而言,这意味着它不能假设a具有某个值,因为它曾经在1皮秒之前拥有该值,并且没有代码似乎已经改变了它。没关系。访问a时,始终读取其当前值。

过度使用volatile会阻止大量的编译器优化,可能会大大减慢计算代码的速度,并且人们常常在不需要的情况下使用volatile。例如,编译器从不对内存障碍进行值假设。究竟什么是内存障碍?嗯,这远远超出了我的回复范围。您只需要知道典型的同步结构是内存障碍,例如:锁,互斥锁或信号量等。请考虑以下代码:

a

// Global Scope int a = 0; void function ( ) { a = 24; pthread_mutex_lock(m); b = a + 3; pthread_mutex_unlock(m); } 是一个内存屏障(顺便说一下pthread_mutex_lock),因此没有必要将pthread_mutex_unlock声明为a,编译器不会假设volatile跨越记忆障碍的价值,从不

在所有这些方面,Objective-C与C非常相似,毕竟它只是一个带有扩展和运行时的C语言。需要注意的一点是,Obj-C中的a属性是内存障碍,因此您不需要声明属性atomic。如果您从多个线程访问该属性,请声明它volatile,这甚至是默认的(如果您不标记它atomic,它将是nonatomic)。如果你永远不会从多个线程访问它,标记它atomic将更快地访问该属性,但只有当你真正访问该属性时才会得到回报(很多并不意味着十次一分钟,相当于每秒几千次。

所以你想要Obj-C代码,需要volatile?

nonatomic

如果没有@implementation SomeObject { volatile bool done; } - (void)someMethod { done = false; // Start some background task that performes an action // and when it is done with that action, it sets `done` to true. // ... // Wait till the background task is done while (!done) { // Run the runloop for 10 ms, then check again [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01] ]; } } @end ,编译器可能会愚蠢地假设,volatile永远不会在此更改,只需用done替换!donetrue是一个永无止境的无限循环。

我还没有用现代编译器对它进行测试。也许当前版本的while (true)比这更聪明。它还可能取决于您如何启动后台任务。如果您调度一个块,编译器实际上可以很容易地看到它是否更改clang。如果您在某处传递对done的引用,则编译器知道接收方可能具有done的值,并且不会做出任何假设。但是很久以前,当Apple仍在使用GCC 2.x并且没有使用done时,我确实测试了那段代码,并且没有使用volatile真正导致永无止境的无限循环(但仅在启用了优化的发布版本中,而不是在调试版本中) )。所以我不会依赖编译器足够聪明来做正确的事。


关于内存障碍的更多有趣事实:

如果您曾查看过Apple在<libkern/OSAtomic.h>中提供的原子操作,那么您可能想知道为什么每个操作都存在两次:一次为x,一次为xBarrier(例如OSAtomicAdd32OSAtomicAdd32Barrier)。好吧,现在你终于明白了。与#34;障碍&#34;在它的名字是一个记忆障碍,另一个是不是。

内存障碍不仅适用于编译器,也适用于CPU(存在CPU指令,被视为内存屏障,而普通指令则不然)。 CPU需要知道这些障碍,因为CPU喜欢重新排序指令以无序执行操作。例如。如果你这样做

a = x + 3 // (1)
b = y * 5 // (2)
c = a + b // (3)

并且用于添加的管道很忙,但是用于乘法的管道不是,CPU可以在(2)之前执行指令(1),之后所有订单在最后都不重要。这可以防止管道停滞。此外,CPU非常聪明,知道它无法在(3)(1)之前执行(2),因为(3)的结果取决于其他两个计算的结果。< / p>

然而,某些类型的订单更改会破坏代码或程序员的意图。考虑这个例子:

x = y + z // (1)
a = 1 // (2)

添加管道可能很忙,为什么不在(2)之前执行(1)?他们不相互依赖,订单不重要,对吧?错误!但为什么?因为另一个线程会监视a的更改,一旦a变为1,它就会读取x的值,如果有说明,它现在应为y+z按顺序执行,但如果CPU已重新排序上面的两行,则不是。

因此,在这种情况下,订单将很重要,这就是为什么CPU也需要障碍的原因:CPU不会在这些障碍上订购指令,因此指令(2)需要成为障碍指令(或者(1)(2)之间需要有这样的指令;这取决于CPU)。重新排序指令相当新,但更老的问题是延迟的内存写入。如果CPU延迟了内存写入(对于某些CPU非常常见,因为CPU的内存访问非常慢),它将确保在超过内存屏障之前执行延迟写入(现在您知道名称和#34; 记忆障碍&#34;实际上来自)。

你可能在记忆障碍方面的工作量远远超过你所知道的(GCD - Grand Central Dispatch充满了这些和NSOperation / NSOperationQueue基于GCD),&#39;为什么你真的只需要在非常罕见的例外情况下使用volatile。您可能会忘记编写100个应用程序,甚至不必使用它一次。但是,如果您编写了许多旨在实现最高性能的低级别多线程代码,那么您迟早会遇到只有volatile可以授予您正确行为的情况。

答案 1 :(得分:1)

这里给出了一个很好的解释:Understanding “volatile” qualifier in C

  

volatile关键字旨在防止编译器对可能以编译器无法确定的方式更改的对象应用任何优化。

     

声明为volatile的对象在优化中被省略,因为它们的值可以随时由当前代码范围之外的代码更改。系统始终从内存位置读取易失性对象的当前值,而不是将其值保存在请求点的临时寄存器中,即使前一条指令要求来自同一对象的值也是如此。所以简单的问题是,变量的值如何以编译器无法预测的方式发生变化。考虑以下案例来回答这个问题。

     

1) 由作用域外的中断服务程序修改的全局变量: 例如,全局变量可以表示数据端口(通常称为内存映射的全局指针) IO)将动态更新。必须将代码读取数据端口声明为volatile,以便获取端口上可用的最新数据。如果没有将变量声明为volatile,编译器将优化代码,使其只读取一次端口并在临时寄存器中保持使用相同的值来加速程序(速度优化)。通常,当由于新数据的可用性而存在中断时,ISR用于更新这些数据端口

     

2)多线程应用程序中的全局变量:线程通信有多种方式,即消息传递,共享内存,邮箱等。全局变量是共享的弱形式记忆。当两个线程通过全局变量共享信息时,它们需要使用volatile进行限定。由于线程是异步运行的,因此应该由另一个消费者线程新获取由于一个线程引起的全局变量的任何更新。编译器可以读取全局变量,并可以将它们放在当前线程上下文的临时变量中。要使编译器优化的效果无效,这些全局变量将被限定为volatile

     

如果我们不使用挥发性限定符,可能会出现以下问题   1)打开优化时,代码可能无法正常工作   2)启用和使用中断时,代码可能无法正常工作。

答案 2 :(得分:0)

volatile来自C. Type&#34; C语言不稳定&#34;进入你最喜欢的搜索引擎(一些结果可能来自SO),或读一本关于C编程的书。那里有很多例子。