哪个场景关键字“volatile”需要在objective-c中声明?
据我所知, volatile
通常用于防止某些硬件操作期间意外的编译优化。 但是,在属性定义中应该声明哪些场景是volatile
。 请举一些有代表性的例子。
谢谢。
编译器假定variables可以改变其值的唯一方法是通过改变它的代码。
int a = 24;
现在编译器假定a
是24
直到看到任何改变a的值的语句。 如果你在上面声明的下面写代码
int b = a + 3;
编译器会说:“ 我知道是什么,它是24
所以b
是27
,我不必编写代码来执行这个计算,我知道它总是 27
”。 编译器可能只是优化整个计算。
但是如果在赋值和计算之间发生了变化,编译器就会出错。 但是,为什么会这样做呢? 为什么突然有一个不同的价值? 它不会。
如果a
是一个堆栈variables,它不能改变值,除非你传递一个引用,例如
doSomething(&a);
函数doSomething
有一个指向a的指针,这意味着它可以改变a的值,并且在那行代码之后, a
可能不再是24
。 所以,如果你写
int a = 24; doSomething(&a); int b = a + 3;
编译器不会优化计算。 谁知道做什么后会有什么价值? 编译器肯定不会。
对象的全局variables或实例variables会让事情变得更加棘手。 这些variables不在堆栈上,它们在堆上,这意味着不同的线程可以访问它们。
// Global Scope int a = 0; void function ( ) { a = 24; b = a + 3; }
b
会27
吗? 最有可能的答案是肯定的,但是有一些其他线程已经改变了这两行代码之间的值的一个小的机会,然后它不会是27
。 编译器是否在意? 没有为什么? 因为C对线程一无所知 – 至less它并不习惯(最新的C标准最终知道本地线程,但之前的所有线程function只是由操作系统提供的API,而不是本地C)。 所以C编译器仍然会假设b
是27
并且优化计算,这可能会导致错误的结果。
这就是volatile
好的原因。 如果你像这样标记一个variablesvolatile
volatile int a = 0;
你基本上是在告诉编译器:“a 的价值随时都可能发生变化,没有认真的,它可能会变成蓝色,你不会看到它来,* bang *,它有不同的价值! 对于编译器来说,这意味着它不能仅仅因为它曾经有一个皮秒的值,而且没有代码似乎已经改变了它,所以不能假设它有一个特定的值。 没关系。 访问a
,请始终读取其当前值。
过度使用volatile可以防止大量的编译器优化,可能会大大减慢计算代码的速度,并且很多时候人们在甚至不需要的情况下使用volatile。 例如,编译器从不在内存屏障上进行价值假设。 内存屏障究竟是什么? 那么,这有点远远超出了我的答复范围。 您只需要知道典型的同步构造是内存屏障,例如锁,互斥锁或信号量等。考虑以下代码:
// 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中的atomic
属性是内存屏障,所以你不需要声明属性volatile
。 如果你从多个线程访问属性,声明它的atomic
,甚至默认的方式(如果你不标记它nonatomic
,它将是atomic
)。 如果你从来没有从多个线程访问它,把它标记nonatomic
将使访问该属性的速度要快得多,但是只有当你访问属性的时候才能得到回报(很多并不意味着每分钟十次,而是几个每秒千次)。
所以你想要Obj-C的代码,这需要挥发?
@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
永远不会改变,取而代之!done
while (true)
是一个无止境的循环,永远不会终止。
我还没有用现代编译器testing过。 也许当前版本的clang
比这更聪明。 这也可能取决于你如何开始后台任务。 如果你调度一个块,编译器可以很容易的看到它是否改变done
。 如果你通过一个引用来done
某个地方,编译器知道接收者可能done
的值,不会做出任何假设。 但是我很久以前在Apple使用GCC 2.x的时候testing了这个代码,并且没有使用volatile
实际上导致了一个永不终止的无限循环(但是只有在启用了优化的版本中,而不是在debugging版本中)。 所以我不会依赖于编译器足够聪明的做对了。
只是一些关于记忆障碍的有趣事实:
如果你曾经看过苹果在<libkern/OSAtomic.h>
提供的primefaces操作,那么你可能会想知道为什么每一个操作都存在两次:一次是x
,一次是xBarrier
(例如OSAtomicAdd32
和OSAtomicAdd32Barrier
)。 那么,现在你终于知道了。 一个名为“屏障”的人是一个记忆障碍,另一个则不是。
内存障碍不仅仅针对编译器,它们也是CPU(存在CPU指令,被认为是内存障碍,而正常的指令则不存在)。 CPU需要知道这些障碍,因为CPU喜欢重新排列指令来执行乱序操作。 如果你这样做
a = x + 3 // (1) b = y * 5 // (2) c = a + b // (3)
且加法stream水线繁忙,但是乘法stream水线不是,则CPU可以在(1)
之前执行指令(2)
(1)
,最后所有的指令都不重要。 这可以防止pipe道堵塞。 另外,由于(3)
的结果取决于另外两个计算的结果,所以CPU足够聪明以知道它不能在(1)
或(2)
之前执行(3)
。
然而,某些种类的订单更改会破坏代码或程序员的意图。 考虑这个例子:
x = y + z // (1) a = 1 // (2)
添加pipe道可能很忙,为什么不在(2)
之前执行(2)
(1)
? 他们不相互依赖,顺序不要紧,对不对? 错误! 但为什么? 因为另一个线程监视a
变化,一旦变为1
,它读取x
的值,如果指令是按顺序执行的话,它现在应该是y+z
,但是如果CPU重新排列了两条线以上。
因此,在这种情况下,订单将很重要,这就是为什么CPU也需要屏障:CPU不会跨越这些障碍来排列指令,因此指令(2)
将需要成为屏障指令(或者需要这样的指令(1)
和(2)
;这取决于CPU)。 重新sorting指令是相当新的,但是,更老的问题是延迟内存写入。 如果CPU延迟内存写入(对于某些CPU非常常见,因为CPU的内存访问非常慢),它将确保延迟写入在内存屏障越过之前执行(现在您知道名称“ 内存屏障 ”实际上来自)。
你可能在记忆障碍方面做的工作比你所知NSOperation
(GCD – Grand Central Dispatch充满了这些和NSOperation
/ NSOperationQueue
基础),这就是为什么你真的需要使用volatile
只有在非常罕见的例外情况下。 您可能会忘记编写100个应用程序,甚至无需使用它。 但是,如果您编写了大量低级别的multithreading代码,旨在尽可能实现最高性能,那么您迟早会遇到一种情况,即只有volatile
才能授予您正确的行为。
这里给出一个很好的解释: 理解C中的“volatile”限定符
volatile关键字旨在防止编译器对可能以编译器无法确定的方式更改的对象应用任何优化。
声明为volatile的对象从优化中省略,因为它们的值可以随时通过当前代码范围之外的代码进行更改。 即使先前的指令要求来自同一对象的值,系统总是从内存位置读取易失性对象的当前值,而不是将其值保存在临时寄存器中。 所以简单的问题是,一个variables的值如何以编译器无法预测的方式改变。 考虑以下情况来回答这个问题。
1) 范围之外的中断服务程序修改的全局variables:例如,一个全局variables可以表示一个数据端口(通常是全局指针,被称为内存映射IO),将被dynamic更新。 读取数据端口的代码必须声明为volatile,才能获取端口上的最新数据。 无法将variables声明为volatile,编译器将优化代码,使其只读取一次端口,并在临时寄存器中使用相同的值来加速程序(速度优化)。 通常情况下,由于新数据的可用性,当出现中断时,用于更新这些数据端口的ISR
2) multithreading应用程序中的全局variables:线程通信有多种方式,即消息传递,共享内存,邮箱等。全局variables是弱forms的共享内存。 当两个线程通过全局variables共享信息时,需要用volatile来限定。 由于线程asynchronous运行,任何由于一个线程而导致的全局variables更新应该由另一个消费者线程新鲜地获取。 编译器可以读取全局variables,并可以将它们放置在当前线程上下文的临时variables中。 为了使编译器优化的效果无效,将这些全局variables限定为易失性
如果我们不使用volatile限定符,可能会出现以下问题
1)开启优化时,代码可能无法正常工作。
2)中断启用和使用时,代码可能无法正常工作。
来自C的不稳定。在你最喜欢的search引擎中键入“C语言易失性”(一些结果可能来自SO),或者阅读一本关于C编程的书。 那里有很多例子。