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

dad*_*eon 14 objective-c keyword ios

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

谢谢.

Mec*_*cki 46

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

int a = 24;
Run Code Online (Sandbox Code Playgroud)

现在,编译器假定a24,直到它看到的值改变任何声明a.如果你在上面的语句下面写代码那么说

int b = a + 3;
Run Code Online (Sandbox Code Playgroud)

编译器会说" 我知道a它是什么,它是24!所以b27.我不必编写代码来执行该计算,我知道它将永远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)

b27?很可能答案是肯定的,但是其他一些线程很可能改变了a这两行代码之间的值,然后就不会这样了27.编译器在乎吗?没有为什么?因为C对线程一无所知 - 至少它不习惯(最新的C标准最终知道本机线程,但之前的所有线程功能只是操作系统提供的API而不是C本机).所以C编译器仍然会假设这b27并且优化计算,这可能导致不正确的结果.

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顺便说一句),因此没有必要声明avolatile,编译器不会假设a跨越内存屏障的值,永远不会.

Objective-C在所有这些方面都非常像C,毕竟它只是一个带有扩展和运行时的C语言.需要注意的一点是,atomicObj-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(例如OSAtomicAdd32OSAtomicAdd32Barrier).好吧,现在你终于明白了.名字中有"障碍"的是一个记忆障碍,另一个则不是.

内存障碍不仅适用于编译器,它们也适用于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/或NSOperationQueueGCD基础),这就是为什么你真的volatile只需要在非常罕见的特殊情况下使用.您可能会忘记编写100个应用程序,甚至不必使用它一次.但是,如果您编写了许多低级别的多线程代码,旨在实现最高性能,那么您迟早会遇到只能volatile授予您正确行为的情况.

  • 事实上,我发现这个答案真的没有得到足够的观众。感谢您分享您的知识,这是我长期以来阅读的最好的“文章”之一。你可以把它发布到objc-io或其他地方,这真的很值得!恭喜,我在 SOF 上找到的关于任何主题的最佳解释。 (2认同)