volatile
关键字的目的是防止编译器对变量访问作任何优化,由于这些变量可能会以编译器没法肯定的方式被修改。html
声明为volatile
的变量不会被优化,由于它们的值随时可能被当前代码范围以外的代码修改。系统老是从内存读取变量的当前值,而不会使用寄存器中的值,即便上条指令刚操做过此数据。(volatile
的影响远不止是否使用寄存器值这么简单)程序员
当变量的值可能发生意外变化时,应该将其声明为volatile。实际上,只有三种状况:objective-c
第一种状况,外围设备寄存器的值随时可能被外部改变,显然超出了代码的范围。第二种状况,中断处理程序的执行模式不一样于普通程序,当中断到来时,当前线程挂起,执行中断处理程序,以后恢复代码的执行。能够认为,中断处理程序与当前程序是并行的,独立于正常代码执行序列以外。第三种状况比较常见,就是通常的并发编程。编程
编译器假设变量值变化的惟一方式是被代码修改。多线程
int a = 24;
复制代码
如今编译器会认为 a
的值一直是24
,除非遇到修改a
值的语句。若是后面有代码:并发
int b = a + 3;
复制代码
编译器会认为,既然已经知道a
的值是24
,所以b
的值确定是27
,因此不须要生成计算a + 3
的指令。app
若是a
的值在两条语句中间被修改了,那么编译的结果就会出错。然而,a
的值为何会忽然被修改呢?不会的。函数
若是a
是一个栈变量,除非传递一个指向它的引用,不然它的值是不会改变的。例如:工具
doSomething(&a);
复制代码
函数doSomething
有一个指向a
的指针,意味着a
的值可能会被修改,此行代码以后a
的值可能就再也不是24了。若是这样写:oop
int a = 24;
doSomething(&a);
int b = a + 3;
复制代码
编译器将不会优化掉a + 3
的计算。谁知道doSomething
以后a
的值是多少呢?编译器显然不知道。
对于全局变量或者对象的实例变量,问题会更复杂一些。这些变量不在栈上,而是在堆中,这意味着不一样的线程能够访问它们。
// Global Scope
int a = 0;
void function() {
a = 24;
b = a + 3;
}
复制代码
b
会是27
吗?极可能是的,不过其它线程也可能在两条语句之间修改了a
的值,尽管这种可能性比较小。编译器会意识到这一点儿吗?不会的。由于C语言自己并不知道关于线程的任何东西——至少过去是这样的(最新的C标准终于知道了native线程,不过以前全部的线程功能都是由操做系统提供的API,而不是C语言自己的特性)。所以C编译器依然会认为b
的值是27,并将计算优化掉,这会致使错误的结果。
这就是volatile
的用武之地了。若是标记变量为volatile:
volatile int a = 0;
复制代码
咱们告诉编译器:a
的值可能随时会忽然改变。对于编译器来讲,这意味着它不能假设a
的值,哪怕1皮秒以前它仍是那个值,而且看起来也没有代码修改它。每次访问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
,编译器不会跨越内存屏障假设a
的值,永远不会。
Objective-C在全部方面都很像C,毕竟它只是一个带有运行时的扩展版的C。须要指出的一点是,atomic
属性是内存屏障,所以不须要为属性声明volatile。若是须要在多个线程中访问属性,那么能够将属性声明为atomic
的(若是不声明nonatomic
,默认也是atomic
)。若是不须要在多个线程访问,标记为nonatomic
会使属性的访问更快,不过只有在频繁访问属性时才会表现出来(不是指一分钟访问10次那种,而是一秒钟须要访问数千次以上)。
Obj-C代码何时须要使用volatile呢?
@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
不会改变,所以简单地用true
替换done
,以至于造成一个死循环。
当Apple还在使用GCC 2.x时,若是上面的代码没有使用volatile,的确会致使死循环(仅在开启优化的release编译模式,debug模式并不会)。在现代编译器上没有验证过这一点,或许当前版本的clang
更智能一些。不过咱们显然不能期望编译器足够聪明来正确处理这一点。 同时也取决于启动后台任务的方式。若是dispatch一个block,编译器很容易知道done
是否会被改变。若是向某处传递一个指向done
的指针,编译器知道done
的值可能会被修改,所以不会对其值进行任何假设。
若是你看过苹果在<libkern/OSAtomic.h>
文件或atomic 的手册页中提供的原子操做,那么或许你已经注意到,每一个操做有两个版本:一个x
和一个xBarrier
(例如,OSAtomicAdd32
和OSAtomicAdd32Barrier
)。如今你知道了,名字中带有“Barrier”的是一个内存屏障,而另外一个不是。
内存屏障不只适用于编译器,也适用于CPU(有些CPU指令被认为是内存屏障)。CPU须要知道这些屏障,由于CPU会对指令从新排序,以便流水线化乱序执行。例如:
a = x + 3; // (1)
b = y * 5; // (2)
c = a + b; // (3)
复制代码
假设加法器的流水线正忙,而乘法器的流水线还有空闲,那么CPU可能会在(1)以前先执行(2),毕竟执行顺序并不会影响最终的运算结果。这能够防止管道停滞。固然,CPU也足够聪明地知道(3)的执行不能早于(1)或(2),由于(3)的结果依赖于(1)和(2)的结果。
流水线,简单来讲就是一个CPU核心有多套运算器,每条指令分为几个阶段,多条指令并行执行。
load instruction decode load instruction load data decode load instruction operation load data decode save data operation load data save data operation save data
然而,某些类型的顺序更改会破坏代码或程序员的意图。考虑以下代码:
x = y + z; // (1)
a = 1; // (2)
复制代码
加法器流水线正忙,所以为何不在(1)以前先执行(2)呢?它们没有依赖关系,所以顺序可有可无,对吧?看状况。假设有一个线程正在监听a
的变化,当a
变为1时,读取x
的值,若是按序执行的话值应该是y + z
。但若是CPU调整了执行顺序,x
的值就仍是此段代码执行以前的值,此时另外一个线程获取到的值是不符合程序员指望的。
对于这种状况,顺序是很重要的,这就是为何CPU也须要屏障:CPU不会跨越屏障从新排序指令。所以,指令(2)须要是一个屏障指令(或者在(1)和(2)之间有一个屏障指令,取决于具体的CPU)。
从新排序指令是现代CPU的特性,还有一个更老的问题是延迟内存写操做。若是CPU延迟对内存的写操做(对于一些CPU来讲很常见,由于内存访问速度相对于CPU来讲实在太慢了),它将确保全部延迟的写操做在跨越内存屏障以前被执行并完成,所以当其它线程访问时,全部的内存都处于正确的状态(知道“内存屏障”这个词的出处了吧)。
与内存屏障打交道的地方可能比咱们意识到的要多不少(GCD - Grand Central Dispatch处处都是内存屏障,以及基于GCD的NSOperation/NSOperationQueue
),这就是为何咱们只须要在很是少的、特殊的状况才真正须要使用volatile
。可能你写了100个App都不须要用到一次。然而,若是咱们须要编写大量低层的、多线程的代码,并指望达到最高的性能,那么或早或晚会遇到必须使用volatile
才能确保功能正确的状况。若是此种状况不使用volatile,可能致使死循环或变量值不正确却没法解释的问题。若是遇到了这样的问题,尤为是只在release模式下才会出现,那么极可能是由于缺失了volatile
或内存屏障。
为了优化代码性能,编译器默认状况下会根据当前代码的上下文推断变量的值,以减小没必要要的计算。在单线程、正常执行的状况下,不会有什么问题。可是在中断处理程序、多线程并发和内存映射I/O的状况,变量的值可能在当前代码范围以外忽然被修改,这些状况超出了编译器的意识范围。所以,须要咱们显式地告诉编译器,不要推断这些变量的值,由于它们随时可能被当前代码范围以外的代码或硬件修改。
另外,编译器不会跨越内存屏障推断变量的值。在实际编程中,不少内存屏障是隐性的,由于常见的同步工具已带有内存屏障功能,如锁、互斥和信号量等,iOS并行编程中最经常使用的GCD处处都是内存屏障,atomic
属性也是一个内存屏障。
屏障不只适用于编译器,也适用于CPU。绝大多数现代CPU都引入了流水线,乱序并行执行多条指令。当咱们想要确保指令执行顺序时,也须要使用屏障指令。CPU不会跨越屏障重排指令顺序。
须要注意的是,C语言自己并无线程的概念,线程是操做系统提供的API,所以编译器不会假设全局变量随时会被其它线程修改。固然,编译器的智能化在不断提升,C语言自己也在进化。不过,咱们仍是不要依赖于编译器的聪明程度为好。