Objective-C CPU缓存行为

10
苹果提供了一些有关同步变量甚至执行顺序的文档。但我没有看到任何有关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);
  });
}

在这种情况下,计数器是否应该是易失性的?

更新1

线程间通信中的文档保证共享变量可以用于线程间通信。

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

这是否意味着在这种情况下不需要使用volatile?这与内存屏障和易失性变量中的文档相矛盾:

然而,如果该变量可从另一个线程中访问,则这样的优化可能会防止其他线程注意到对其所做的任何更改。将易失性关键字应用于变量强制编译器每次使用该变量时都从内存加载它。

所以我还不知道是否需要使用volatile,因为编译器可以使用寄存器缓存优化,或者它不是必需的,因为编译器以某种方式知道它是“共享”的东西。
文档没有很清楚地说明共享变量是什么或编译器如何知道它。在上面的例子中,count是一个共享对象吗?假设count是一个整数,则它不是一个对象。它是共享内存块吗,还是只适用于__block声明的变量?也许对于非块、非对象、非全局的共享变量需要使用volatile。
更新2
对于所有认为这是关于同步的问题的人,这不是。这是关于iOS平台上的CPU缓存行为的问题。

1
简而言之:原子属性保证跨线程和CPU缓存的原子访问,但要小心,因为这并不一定意味着线程安全。例如,self.count += 1 不是一个原子操作(self.count += 1 === self.count = self.count + 1;在读取和写入之间可以发生任何事情)。 - Itai Ferber
1
据我所知,Cocoa 中实现的原子操作确保了跨处理器的一致性,但我不能保证。 - Duncan C
2
顺便提一下,volatile 并不确保您将读取到未缓存的值: https://dev59.com/tXRB5IYBdhLWcg3wpotm,https://dev59.com/J3bZa4cB1Zd3GeqPH6CW,https://dev59.com/6Gsz5IYBdhLWcg3weHuU?rq=1 (它只保证编译器不会优化掉读取操作)。 - Itai Ferber
1
内存屏障和易失变量如果变量可以从另一个线程中访问,这样的优化可能会防止其他线程注意到对它的任何更改。将volatile关键字应用于变量会强制编译器每次使用时从内存中加载该变量。这意味着易失变量可以解决缓存问题,但是这篇文档非常误导且不完整。 - John K
1
我对Objective C一窍不通。但从架构的角度来看,如果您使用锁或原子块保护读操作(主线程)和写操作(后台线程),那么可以保证主线程将看到后台线程的任何更新。仅因为每次只有一个线程访问计数变量,当另一个线程稍后尝试访问它时,一致性协议将会触发并传递更新的值。 - Isuru H
显示剩余13条评论
3个回答

1

我知道你可能在询问跨线程使用变量的一般情况(在这种情况下,使用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 结构体。

这意味着这段代码并不能完全达到您的预期。

-(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已经超过几年,您会记得当您在头文件中声明ObjC属性时必须添加@synthesize propertyName = _ivarName@implementation中。(现在Xcode会自动为您完成)

@synthesize是Xcode生成setter和getter方法的触发器。(如果您没有编写@synthesize,Xcode希望您自己编写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的线程问题,那么您应该担心两个线程同时调用这些方法(而不是直接同时访问同一个变量,因为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;

简而言之

如果您关心线程,并且希望确保在另一个线程上进行写操作时不会读取一半的值,则将nonatomic更改为atomic(或者去掉nonatomic,因为atomic是默认值)。这将导致生成类似于以下代码的代码。

@property (atomic, assign) NSInteger count;

// setter
@synchronized(self) {
    _count = count;
}

这并不能保证你的代码是线程安全的,但只要你只访问属性视图的setter和getter,就可以避免在另一个线程上进行写操作时读取该值的可能性。有关原子和非原子的更多信息,请参阅此问题的答案。

抱歉,您完全没有谈到CPU缓存行为。 - John K

0

在编程中,你应该使用锁或其他同步机制来保护共享变量。根据文档所述:

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

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


Mike的博客是这些概念的很好介绍,但它缺少一个关于os_unfair_lock的部分,这是你在这里应该使用的结构。 - CodaFi
这个问题与原子性无关,而是涉及到iOS上的CPU缓存行为。 - John K

0

最简单的方法,也是对开发者大脑最不具挑战性的方法,就是在串行调度队列上执行任务。串行调度队列,像主队列一样,是多线程世界中的一个小型单线程岛屿。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接