【问题标题】:Objective-C cpu cache behaviorObjective-C cpu 缓存行为
【发布时间】:2017-07-09 04:03:50
【问题描述】:

Apple 提供了一些关于同步变量甚至执行顺序的文档。我没有看到任何关于 CPU 缓存行为的文档。 Objective-C 开发者有什么保证和控制来确保线程之间的缓存一致性?

考虑在后台线程上设置变量但在主线程上读取的以下情况:

self.count = 0;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
  self.count = 5;
  dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"%i", self.count);
  });
}

在这种情况下 count 应该是 volatile 吗?

更新 1

Inter-thread Communication 中的文档保证共享变量可用于线程间通信。

另一个在两个线程之间传递信息的简单方法是使用全局变量、共享对象或共享内存块。

这是否意味着在这种情况下不需要 volatile?这与Memory Barriers and Volatile Variables 中的文档冲突:

如果变量在另一个线程中是可见的,那么这样的优化可能会阻止另一个线程注意到它的任何变化。将 volatile 关键字应用于变量会强制编译器在每次使用该变量时从内存中加载该变量。

所以我仍然不知道是否需要 volatile 是因为编译器可以使用寄存器缓存优化,还是不需要它,因为编译器不知何故知道它是“共享”的东西。

文档对共享变量是什么或编译器如何知道它不是很清楚。在上面的例子中,count 是一个共享对象吗?假设 count 是一个 int,那么它就不是一个对象。它是共享内存块还是仅适用于 __block 声明的变量?非块、非对象、非全局、共享变量可能需要 volatile。

更新 2

对于所有认为这是关于同步的问题的人来说,事实并非如此。这是关于 iOS 平台上的 CPU 缓存行为。

【问题讨论】:

  • tl;dr: 原子属性保证跨线程和 CPU 缓存的原子访问,但要小心,因为这并不一定意味着线程安全。例如,self.count += 1不是原子操作(self.count += 1 === self.count = self.count + 1;读取和写入之间可能发生任何事情)。
  • 据我了解,在 Cocoa 中实现的 atomic 确实可以确保跨处理器的一致性,但不能保证。
  • 顺便说一句,volatile 并不能确保您将读取未缓存的值:stackoverflow.com/questions/558848/…stackoverflow.com/questions/18695120/…stackoverflow.com/questions/7872175/…(它只保证编译器不会优化读取。)
  • Memory Barriers and Volatile Variables "但是,如果变量在另一个线程中可见,那么这样的优化可能会阻止其他线程注意到它的任何变化。将 volatile 关键字应用于变量会强制编译器加载该变量每次使用时都会从内存中更改。”这要么意味着 volatile 修复了缓存问题,要么这是非常具有误导性和不完整的文档。
  • 我一点也不擅长 Objective C。但是从架构的角度来看这个问题,如果你用锁或原子块保护读操作(主线程)和写操作(后台线程),那么可以保证主线程会看到来自后台线程。仅仅是因为,一次只有一个会访问 count 变量,当另一个尝试访问它时,一致性协议将启动并传递更新的值。

标签: ios objective-c multithreading caching cpu-cache


【解决方案1】:

您应该使用锁或其他一些同步机制来保护共享变量。根据它说的文件:

另一个在两个线程之间传递信息的简单方法是使用全局变量、共享对象或共享内存块。尽管共享变量既快速又简单,但它们也比直接消息传递更脆弱。共享变量必须用锁或其他同步机制小心保护,以确保代码的正确性。否则可能会导致竞争条件、数据损坏或崩溃。

实际上保护计数器变量的最好方法是使用原子操作。你可以阅读文章:https://www.mikeash.com/pyblog/friday-qa-2011-03-04-a-tour-of-osatomic.html

【讨论】:

  • Mike 的博客很好地介绍了这些概念,但缺少关于 os_unfair_lock 的部分,您可能应该在这里使用这种结构来实现原子性。
  • 这个问题与原子性无关。这是关于 iOS 上的 CPU 缓存行为。
【解决方案2】:

最简单的方法,也是对开发人员大脑挑战最小的方法,是在串行调度队列上执行任务。与主队列一样,串行调度队列是多线程世界中的一个微小的单线程岛。

【讨论】:

    【解决方案3】:

    我知道您可能会询问跨线程使用变量的一般情况(在这种情况下,关于使用 volatile 和锁的规则对于 ObjC 和普通 C 相同)。但是,对于您发布的示例代码,规则略有不同。 (我将跳过并简化一些事情并使用 Xcode 来表示 Xcode 和编译器)

    self.count = 0;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
      self.count = 5;
      dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"%i", self.count);
      });
    }
    

    我将假设 self 是一个 NSObject 子类,如下所示:

    @interface MyClass : NSObject {
        NSInteger something;
    }
    @property (nonatomic, assign) NSInteger count;
    @end
    

    Objective C 是 C 的超集,如果您曾经对 ObjC 进行过任何逆向工程,您就会知道 ObjC 代码(在某种程度上,不完全是)在编译之前会转换为 C 代码。所有[self method:object] 调用都转换为objc_msgSend(self, "method:", object) 调用,self 是一个 C 结构,其中包含 ivars 和其他运行时信息。

    这意味着这段代码并没有达到您的预期。

    -(void)doThing{
       NSInteger results = something + self.count;
    }
    

    仅访问 something 不仅仅是访问变量,而是在执行 self->something(这就是为什么在访问 Objective C 块中的 ivar 时需要获得对 self 的弱引用以避免保留循环的原因) .

    第二点是Objective C 属性并不真正存在。 self.count 变成 [self count]self.count = 5 变成 [self setCount:5]。 Objective C 属性只是语法糖;方便为您节省一些打字时间,并使事情看起来更好。

    如果您在几年前就已经使用 Objective C,您会记得您必须将 @synthesize propertyName = _ivarName 添加到您在标题中声明的 ObjC 属性的 @implementation 中。 (现在 Xcode 会自动为您完成)

    @synthesize 是 Xcode 为您生成 setter 和 getter 方法的触发器。 (如果你没有写@synthesizeXcode 期望你自己写 setter 和 getter)

    // Auto generated code you never see unless you reverse engineer the compiled binary
    -(void)setCount:(NSInteger)count{
        _count = count;
    }
    -(NSInteger)count{
        return _count;
    }
    

    如果您担心self.count 的线程问题,您担心有 2 个线程同时调用这些方法(不是一次直接访问同一个变量,因为self.count 实际上是方法调用而不是变量)。

    标头中的属性定义会更改生成的代码(除非您自己实现 setter)。

    @property (nonatomic, retain)
    [_count release];
    [count retain];
    _count = count;
    
    @property (nonatomic, copy)
    [_count release];
    _count = [count copy];
    
    @property (nonatomic, assign)
    _count = count;
    

    TLDR

    如果您关心线程并希望确保您不会在另一个线程上发生写入的中途读取该值,则将nonatomic 更改为atomic(或将nonatomic 删除为atomic是默认值)。这将导致代码生成类似这样的内容。

    @property (atomic, assign) NSInteger count;
    
    // setter
    @synchronized(self) {
        _count = count;
    }
    

    这不能保证您的代码是线程安全的,但是(只要您只访问属性视图它的 setter 和 getter)应该意味着您避免在另一个线程上写入期间读取值的可能性。有关原子和非原子的更多信息,请参阅this question 的答案。

    【讨论】:

    • 抱歉,您根本没有谈论 CPU 缓存行为。
    猜你喜欢
    • 1970-01-01
    • 2015-06-20
    • 2013-11-21
    • 2011-12-18
    • 2014-11-22
    • 1970-01-01
    • 2012-04-28
    • 2013-06-12
    • 2023-03-05
    相关资源
    最近更新 更多