【问题标题】:Which scenes keyword "volatile" is needed to declare in objective-c?在objective-c中需要声明哪些场景关键字“volatile”?
【发布时间】:2015-10-10 17:13:12
【问题描述】:

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

谢谢。

【问题讨论】:

    标签: ios objective-c keyword


    【解决方案1】:

    volatile 来自 C。在您最喜欢的搜索引擎中输入“C language volatile”(某些结果可能来自 SO),或者阅读有关 C 编程的书。那里有很多例子。

    【讨论】:

      【解决方案2】:

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

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

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

      1) 由范围外的中断服务程序修改的全局变量: 例如,一个全局变量可以表示一个数据端口(通常称为内存映射的全局指针) IO) 将动态更新。代码读取数据端口必须声明为易失性,以便获取端口上可用的最新数据。未能将变量声明为 volatile,编译器将优化代码,使其仅读取一次端口并继续在临时寄存器中使用相同的值来加速程序(速度优化)。一般来说,ISR 用于在由于新数据的可用性而出现中断时更新这些数据端口

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

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

      【讨论】:

        【解决方案3】:

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

        int a = 24;
        

        现在编译器假定a24,直到它看到任何更改a 值的语句。如果您在上述语句下方的某处编写代码,则说明

        int b = a + 3;
        

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

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

        如果a是一个栈变量,它不能改变值,除非你传递一个对它的引用,例如

        doSomething(&a);
        

        函数doSomething 有一个指向a 的指针,这意味着它可以更改a 的值,并且在那行代码之后,a 可能不再是24。所以如果你写

        int a = 24;
        doSomething(&a);
        int b = a + 3;
        

        编译器不会优化计算。谁知道adoSomething 之后会有什么价值?编译器肯定不会。

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

        // Global Scope
        int a = 0;
        
        void function ( ) {
            a = 24;
            b = a + 3;
        }
        

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

        这就是volatile 的优势所在。如果你像这样标记一个变量 volatile

        volatile int a = 0;
        

        你基本上是在告诉编译器:“a 的值可能随时改变。不严重,它可能会突然改变。你看不到它的到来,*bang*,它有不同的价值!”。对于编译器来说,这意味着它不能假设 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。需要注意的一点是,Obj-C 中的atomic 属性是内存屏障,因此您不需要声明属性volatile。如果您从多个线程访问该属性,请将其声明为atomic,顺便说一下,这甚至是默认值(如果您不标记它nonatomic,它将是atomic)。如果您从不从多个线程访问它,将其标记为nonatomic 将使访问该属性的速度更快,但只有当您真正访问该属性时才会得到回报(很多并不意味着每分钟十次,而是而是每秒几千次)。

        所以你想要需要 volatile 的 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 替换为true。而while (true) 是一个永远不会终止的无限循环。

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


        关于记忆障碍的更多有趣事实:

        如果您看过 Apple 在 <libkern/OSAtomic.h> 中提供的原子操作,那么您可能想知道为什么每个操作都存在两次:一次是 x,一次是 xBarrier(例如 OSAtomicAdd32 和 @ 987654383@)。好吧,现在你终于知道了。名称中带有“Barrier”的一个是内存屏障,另一个不是。

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

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

        加法的流水线很忙,而乘法的流水线不忙,CPU可能会在(1)之前执行指令(2),毕竟顺序到最后都不重要了。这可以防止流水线停顿。 CPU 也很聪明,知道它不能在 (1)(2) 之前执行 (3),因为 (3) 的结果取决于其他两个计算的结果。

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

        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 充满了这些,NSOperation/NSOperationQueue 基于 GCD),这就是您真正需要使用 @ 987654405@ 仅在非常罕见的特殊情况下。您可能会编写 100 个应用程序而无需使用它一次。但是,如果您编写大量旨在实现最大性能的低级多线程代码,您迟早会遇到只有volatile 才能授予您正确行为的情况;在这种情况下不使用它会导致奇怪的错误,其中循环似乎没有终止,或者变量似乎只是具有不正确的值,而您对此没有任何解释。如果您遇到此类错误,尤其是仅在发布版本中看到它们时,您可能会错过 volatile 或代码中某处的内存屏障。

        【讨论】:

        • 事实上我发现这个答案真的没有得到足够的观众。感谢您分享您的知识,这是我很长时间以来读过的最好的“文章”之一。您可以将其发布到 objc-io 或其他地方,这真的很值得!恭喜,这是我在 SOF 上找到的关于任何主题的最佳解释。
        • 哇!什么解释!!!非常感谢!顺便说一句,根据我 7 年开发 ios 的经验,我想我只使用过一次 :) 并且我不时编写多线程代码。
        最近更新 更多