dad*_*eon 14 objective-c keyword ios
据我所知,volatile
通常用于防止在某些硬件操作期间出现意外的编译优化.但是volatile
应该在属性定义中声明哪些场景让我感到困惑.请举几个有代表性的例子.
谢谢.
Mec*_*cki 46
编译器假定变量可以更改其值的唯一方法是通过更改它的代码.
int a = 24;
Run Code Online (Sandbox Code Playgroud)
现在,编译器假定a
是24
,直到它看到的值改变任何声明a
.如果你在上面的语句下面写代码那么说
int b = a + 3;
Run Code Online (Sandbox Code Playgroud)
编译器会说" 我知道a
它是什么,它是24
!所以b
是27
.我不必编写代码来执行该计算,我知道它将永远是27
".编译器可能只是优化整个计算.
但是如果a
在赋值和计算之间发生了变化,编译器就会出错.但是,为什么会a
那样做呢?为什么a
突然有不同的价值?它不会.
如果a
是堆栈变量,则它不能更改值,除非您传递对它的引用,例如
doSomething(&a);
Run Code Online (Sandbox Code Playgroud)
该函数doSomething
有一个指向a
,这意味着它可以更改a
代码行的值和之后的值,a
可能24
不再是.所以,如果你写
int a = 24;
doSomething(&a);
int b = a + 3;
Run Code Online (Sandbox Code Playgroud)
编译器不会优化计算.谁知道什么样的价值a
将有后doSomething
?编译器肯定没有.
使用全局变量或对象的实例变量,事情变得更加棘手.这些变量不在堆栈上,它们位于堆上,这意味着不同的线程可以访问它们.
// Global Scope
int a = 0;
void function ( ) {
a = 24;
b = a + 3;
}
Run Code Online (Sandbox Code Playgroud)
会b
是27
?很可能答案是肯定的,但是其他一些线程很可能改变了a
这两行代码之间的值,然后就不会这样了27
.编译器在乎吗?没有为什么?因为C对线程一无所知 - 至少它不习惯(最新的C标准最终知道本机线程,但之前的所有线程功能只是操作系统提供的API而不是C本机).所以C编译器仍然会假设这b
是27
并且优化计算,这可能导致不正确的结果.
这volatile
就是有益的.如果你像这样标记变量volatile
volatile int a = 0;
Run Code Online (Sandbox Code Playgroud)
你基本上是在告诉编译器:" 价值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);
}
Run Code Online (Sandbox Code Playgroud)
pthread_mutex_lock
是一个内存屏障(pthread_mutex_unlock
顺便说一句),因此没有必要声明a
为volatile
,编译器不会假设a
跨越内存屏障的值,永远不会.
Objective-C在所有这些方面都非常像C,毕竟它只是一个带有扩展和运行时的C语言.需要注意的一点是,atomic
Obj-C中的属性是内存障碍,因此您不需要声明属性volatile
.如果您从多个线程访问该属性,请声明它atomic
,这通常是默认的(如果您没有标记它nonatomic
,它将是atomic
).如果你从来没有从多个线程访问它,标记它将nonatomic
更快地访问该属性,但只有你真正访问该属性时才会得到回报(很多并不意味着每分钟十次,而是几千一秒钟).
所以你想要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
Run Code Online (Sandbox Code Playgroud)
如果没有volatile
,编译器可能会愚蠢地假设,done
在这里永远不会改变并!done
简单地替换true
.并且while (true)
是一个永无止境的无限循环.
我还没有用现代编译器测试过.也许当前版本clang
比这更智能.它还可能取决于您如何启动后台任务.如果您发送一个块,编译器实际上可以很容易地看到它是否发生了变化done
.如果您将引用传递给done
某个地方,编译器会知道接收方可能done
会做出值并且不会进行任何假设.但是很久以前,当Apple仍然在使用GCC 2.x并且没有使用volatile
真正导致无限循环从未终止时(但仅在启用了优化的发布版本中,而不是在调试版本中),我完全测试了该代码.所以我不会依赖编译器足够聪明来做正确的事情.
关于内存障碍的一些更有趣的事实:
如果您曾经看过Apple提供的原子操作<libkern/OSAtomic.h>
,那么您可能想知道为什么每个操作都存在两次:一次为x
一次xBarrier
(例如OSAtomicAdd32
和OSAtomicAdd32Barrier
).好吧,现在你终于明白了.名字中有"障碍"的是一个记忆障碍,另一个则不是.
内存障碍不仅适用于编译器,它们也适用于CPU(存在CPU指令,被认为是内存屏障,而普通指令则不然).CPU需要知道这些障碍,因为CPU喜欢重新排序指令以无序执行操作.例如,如果你这样做
a = x + 3 // (1)
b = y * 5 // (2)
c = a + b // (3)
Run Code Online (Sandbox Code Playgroud)
并且用于加法的管道很忙,但是用于乘法的管道不是,CPU可以在(2)
之前执行指令(1)
,之后所有的命令都无关紧要.这可以防止管道停滞.此外,CPU非常聪明,知道它(3)
之前无法执行,(1)
或者(2)
因为结果(3)
取决于其他两个计算的结果.
然而,某些类型的订单更改将破坏代码或程序员的意图.考虑这个例子:
x = y + z // (1)
a = 1 // (2)
Run Code Online (Sandbox Code Playgroud)
添加管道可能很忙,为什么不在(2)
之前执行(1)
呢?他们不依赖对方,顺序应该不重要吧?错误!但为什么?因为另一个线程监视a
更改,并且一旦a
变为1
,它就会读取值x
,y+z
如果指令按顺序执行,现在应该是,但如果CPU重新排序了上面的两行,则不会.
因此,在这种情况下,顺序将很重要,这就是为什么CPU也需要障碍:CPU不会在这些障碍中排序指令,因此指令(2)
需要成为屏障指令(或者需要在(1)
和之间有这样的指令(2)
;取决于CPU).重新排序指令相当新,但更老的问题是延迟的内存写入.如果CPU延迟了内存写入(对于某些CPU非常常见,因为CPU的内存访问非常慢),它将确保在超过内存屏障之前执行延迟写入(现在您知道名称" 内存屏障 "的位置)实际上来自).
你可能在内存障碍方面的工作量远远超过你所知道的(GCD - Grand Central Dispatch充满了这些和NSOperation
/或NSOperationQueue
GCD基础),这就是为什么你真的volatile
只需要在非常罕见的特殊情况下使用.您可能会忘记编写100个应用程序,甚至不必使用它一次.但是,如果您编写了许多低级别的多线程代码,旨在实现最高性能,那么您迟早会遇到只能volatile
授予您正确行为的情况.
归档时间: |
|
查看次数: |
3754 次 |
最近记录: |