原子属性和非原子属性有什么区别?

1945

在属性声明中,atomicnonatomic是什么意思?

@property(nonatomic, retain) UITextField *userName;
@property(atomic, retain) UITextField *userName;
@property(retain) UITextField *userName;

这三者之间的操作区别是什么?


4
https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/Multithreading/Introduction/Introduction.html#//apple_ref/doc/uid/10000057i - SirRupertIII
28个回答

1807
最后两个是相同的;"atomic"是默认行为(请注意,它实际上并不是关键字;它仅由缺少nonatomic指定 - 最近版本的llvm / clang中添加了atomic作为关键字)。
假设您正在@synthesize方法实现,atomic与非原子会更改生成的代码。如果您编写自己的setter/getters,atomic/nonatomic/retain/assign/copy只是建议性的。(注意:在最近版本的LLVM中,默认行为是@synthesize。也没有必要声明实例变量;它们也将自动合成,并且将在其名称前面加上_以防止意外直接访问)。
使用"atomic",合成的setter/getter将确保始终从getter返回整个值或被setter设置的值,而不管其他线程上的setter活动如何。也就是说,如果线程A在调用getter时,线程B调用setter,那么一个实际可用的值(很可能是自动释放的对象)将返回给A的调用者。
在nonatomic中,不会做出任何此类保证。因此,nonatomic比"atomic"快得多。
"atomic"不保证任何线程安全。如果线程A同时调用getter和线程B和C调用具有不同值的setter,则线程A可能会返回三个值中的任何一个值--先于调用任何setter的值或传递到B和C setter中的任何值中的任何一个值。同样,对象可能会最终具有从B或C得到的值,无法确定。
确保数据完整性(多线程编程的主要挑战之一)是通过其他方式实现的。
此外:
单个属性的“原子性”在涉及多个相关属性时也不能保证线程安全。
考虑:
 @property(atomic, copy) NSString *firstName;
 @property(atomic, copy) NSString *lastName;
 @property(readonly, atomic, copy) NSString *fullName;
在这种情况下,线程A可能通过调用setFirstName:然后调用setLastName:来重命名对象。与此同时,线程B可能会在线程A的两次调用之间调用fullName,并将得到新的名字和旧姓氏组合起来的结果。
为了解决这个问题,您需要一个事务性模型。也就是说,需要一些其他类型的同步和/或排除,以允许在更新相关属性时排除对fullName的访问。

22
既然任何线程安全的代码都会进行自己的锁定等操作,那么什么情况下您希望使用原子属性访问器呢?我很难想出一个好的例子。 - Daniel Dickison
8
@bbum的评论很有道理。我喜欢你对另一个回答的评论,即线程安全更多是一个模型级别的关注点。来自IBM线程安全定义的观点是:“如果一个类被正确实现,也就是说它符合其规范,那么在该类的对象上执行的任何一系列操作(读取或写入公共字段和调用公共方法)都不应该将对象置于无效状态、观察到对象处于无效状态或违反类的任何不变量、前置条件或后置条件。” - Ben Flynn
6
这是一个类似于@StevenKramer的例子:我有一个@property NSArray* astronomicalEvents;,它列出了我想在UI中显示的数据。当应用程序启动时,指针指向一个空数组,然后应用程序从Web上获取数据。当Web请求完成(在不同的线程中)时,应用程序会构建一个新数组,然后原子地将属性设置为新的指针值。这是线程安全的,我不必编写任何锁定代码,除非我漏掉了什么。对我来说看起来相当有用。 - bugloaf
13
在某些架构上(记不清是哪个),将64位值作为参数传递时,可能会一半存放在寄存器中,另一半存放在堆栈中。使用“atomic”可以防止跨线程读取半个值。 (这是一个有趣的错误追踪过程。) - bbum
8
@congliu 线程A返回一个对象,没有使用retain/autorelease技巧。线程B释放该对象。线程A出现问题。atomic确保线程A对返回值有一个强引用(即+1的保留计数)。 - bbum
显示剩余16条评论

366

这在苹果的文档中有详细解释,但以下是一些实际发生的情况示例。

请注意,没有 "atomic" 关键字,如果你不指定 "nonatomic",那么属性是原子的,但显式指定 "atomic" 将导致错误。

如果您不指定 "nonatomic",则属性是原子的,但如果您想要的话,最近的版本仍然可以显式指定 "atomic"。

//@property(nonatomic, retain) UITextField *userName;
//Generates roughly

- (UITextField *) userName {
    return userName;
}

- (void) setUserName:(UITextField *)userName_ {
    [userName_ retain];
    [userName release];
    userName = userName_;
}

现在,原子变量的情况要复杂一些:

//@property(retain) UITextField *userName;
//Generates roughly

- (UITextField *) userName {
    UITextField *retval = nil;
    @synchronized(self) {
        retval = [[userName retain] autorelease];
    }
    return retval;
}

- (void) setUserName:(UITextField *)userName_ {
    @synchronized(self) {
      [userName_ retain];
      [userName release];
      userName = userName_;
    }
}

简单来说,原子版本必须获取锁才能保证线程安全,同时还会增加对象的引用计数(以及自动释放计数器以平衡它),这样可以确保对象存在于调用者中,否则如果另一个线程正在设置值,可能会出现竞争条件,导致引用计数降为0。

实际上,这些东西有很多不同的变体,具体取决于属性是标量值还是对象,以及如何处理保留、复制、只读、非原子等相互作用。总的来说,属性合成器知道如何对所有组合做出“正确的事情”。


8
@Louis Gerbarg: 我相信你提供的 (nonatomic, retain) setter 如果尝试分配相同的对象(即:userName == userName_),将无法正常工作。 - Florin
5
你的代码有点误导人,原子 Getters/Setters 并没有保证同步。尤其是 @property (assign) id delegate; 没有在任何东西上同步(iOS SDK GCC 4.2 ARM -Os),这意味着 [self.delegate delegateMethod:self];foo.delegate = nil; self.foo = nil; [super dealloc]; 之间存在竞争。详见 https://dev59.com/ZnNA5IYBdhLWcg3wjOlS。 - tc.
@fyolnish 我不确定 _val/val 是什么,但是不是。原子 copy/retain 属性的 getter 需要确保它不会返回一个对象,因为在另一个线程中调用了 setter 导致其引用计数变为零,这基本上意味着它需要读取 ivar,保留它同时确保 setter 没有覆盖并释放它,然后自动释放它以平衡保留。这基本上意味着 gettersetter 都必须使用锁定(如果内存布局固定,则应该可以使用 CAS2 指令;遗憾的是 -retain 是一个方法调用)。 - tc.
@tc 已经有一段时间了,但我想写的可能是这个:https://gist.github.com/fjolnir/5d96b3272c6255f6baae 但是,在 setFoo: 返回之前,旧值可能会被读取并在读取器返回之前释放。但是,如果 setter 使用 -autorelease 而不是 -release,那么可能会解决这个问题。 - Fjölnir
很遗憾,不行:它在setter的线程上自动释放,而需要在getter的线程上自动释放。此外,由于使用递归,似乎存在(微小的)堆栈耗尽的可能性。 - tc.
我们能否修复这个答案以回应@Florin的评论?我也注意到了同样的问题并来到这里发表评论。 - kevlar

177

原子操作

  • 是默认行为
  • 确保CPU在另一个进程访问变量之前完成当前进程的操作
  • 速度较慢,因为它完全确保了进程的操作

非原子操作

  • 不是默认行为
  • 速度较快(适用于使用@property和@synthesize创建的变量的合成代码)
  • 不支持多线程安全
  • 当两个不同进程同时访问同一变量时,可能会导致意外行为

143
理解这种差异的最好方法是使用以下示例。
假设有一个名为“name”的原子字符串属性,如果您从线程A调用[self setName:@"A"],从线程B调用[self setName:@"B"],并从线程C调用[self name],则不同线程上的所有操作都将按顺序执行,这意味着如果一个线程正在执行设置器或获取器,则其他线程将等待。
这使属性“name”具有读/写安全性,但如果另一个线程D同时调用[name release],则此操作可能会导致崩溃,因为此处没有涉及设置器/获取器调用。这意味着对象具有读/写安全性(原子性),但不具备线程安全性,因为其他线程可以同时向对象发送任何类型的消息。开发人员应确保对此类对象进行线程安全性。
如果属性“name”为非原子性,则上述示例中的所有线程 - A、B、C和D都将同时执行,从而产生任何不可预测的结果。在原子性情况下,A、B或C中的一个将首先执行,但仍然可以并行执行D。

120

语法和语义已经由其他优秀的答案详细解释了。因为“执行”和“性能”没有被充分详细说明,所以我想要补充我的答案。

这3种属性之间的功能差异是什么?

我一直认为原子属性作为默认选项相当奇怪。在我们工作的抽象级别上,使用原子属性作为实现100%线程安全的手段是一个特例。对于真正正确的多线程程序,几乎肯定需要程序员干预。同时,性能特征和执行尚未被深入详述。多年来,我编写了一些高度多线程的程序,我一直将我的属性声明为nonatomic,因为原子属性对任何目的都不明智。在讨论原子和非原子属性的细节时,我遇到了一些有趣的结果(链接)

执行

首先,我想澄清的第一件事是锁定实现是由实现定义的且被抽象化的。Louis 在他的示例中使用了@synchronized(self),我曾看到过这是一种常见的困惑源泉。实现实际上不使用@synchronized(self);它使用对象级的自旋锁。Louis 的示例对于使用我们都熟悉的构造进行高层次说明非常好,但是了解它并不使用@synchronized(self)很重要。

另一个区别是原子属性将在getter方法中保留/释放您的对象。

性能

有趣的是,在非争用(例如单线程)情况下使用原子属性访问的性能在某些情况下确实非常快。在不太理想的情况下,使用原子访问的开销可能比nonatomic的开销要高出20倍以上。其中使用7个线程的争用情况,对于三字节结构来说慢了44倍(2.2 GHz的Core i7 Quad Core,x86_64)。三字节结构是一个非常慢的属性的例子。

有趣的是三字节结构的用户定义访问器比合成的原子访问器快52倍;或者相当于合成的非原子访问器的84%的速度。

在竞争的情况下,对象的开销也可能超过50倍。

由于优化和实现的变化很多,所以在这些情况下测量真实世界的影响是相当困难的。你经常会听到类似于“除非你通过分析发现它是一个问题,否则就信任它”。由于抽象级别,实际上很难测量实际的影响。从分析数据中获取实际成本可能非常耗时,并且由于抽象层次的原因,结果也可能不太准确。此外,ARC与MRC也会产生很大的差异。

因此,让我们回到高层次的结果,关注属性访问的实现细节,我们将包括像objc_msgSend这样的常见操作,并检查在非争用情况下多次调用NSString获取器的一些真实世界的高层次结果(单位为秒):

  • MRC | nonatomic | 手动实现getter:2
  • MRC | nonatomic | 合成getter:7
  • MRC | atomic | 合成getter:47
  • ARC | nonatomic | 合成getter:38(注意:ARC在这里添加了引用计数轮换)
  • ARC | atomic | 合成getter:47

你可能已经猜到了,在原子操作和自动引用计数(ARC)中,引用计数的活动/循环是一个重要的贡献因素。在有争议的情况下,你也会看到更大的差异。

尽管我非常关注性能,但我仍然说先确保语义! 同时,对于许多项目而言,性能并不是优先考虑的问题。 然而,了解所使用的技术的执行细节和成本肯定是有帮助的。 你应该根据你的需求、目的和能力选择正确的技术。希望这能为你节省几个比较的小时,并帮助你在设计程序时做出更好的决策。


MRC | atomic | synthesized getter: 47 ARC | atomic | synthesized getter: 47 它们有什么相同之处?ARC 不应该有更多开销吗? - SDEZero
2
那么,如果原子属性不好,为什么它们是默认的?为了增加样板代码吗? - Kunal Balani
@LearnCocos2D 我刚在同一台机器上测试了10.8.5,针对10.8的单线程非争用情况下使用一个不是不朽的NSString-ARC原子操作(基准):100% -ARC非原子操作,合成:94% -ARC非原子操作,用户定义:86% -MRC非原子操作,用户定义:5% -MRC非原子操作,合成:19% -MRC原子操作:102% -- 今天的结果有些不同。我没有进行任何synchronized比较。@synchronized在语义上是不同的,如果您有非平凡的并发程序,我不认为它是一个好工具。如果需要速度,请避免使用synchronized - justin
你有这个测试在线上吗?我一直在这里添加我的:https://github.com/LearnCocos2D/LearnCocos2D/tree/master/Cocos2D-Performance-Test - CodeSmile
我总是觉得有趣的是,人们会争论是否要让某个东西变得非常快,或者是50倍的速度,但在这两者之间没有人能察觉到差别。这就像拥有一个视网膜屏幕和另一个分辨率高出50倍的屏幕一样。如果这种性能水平对任何人都没有影响,为什么要浪费资源呢?特别是当健壮的代码可以节省数天的调试时间时... - Bradley Thomas

97

原子性 = 线程安全

非原子性 = 不具备线程安全性

线程安全:

如果实例变量在多个线程中正确访问时,不受运行时环境的调度或交错执行的影响,并且在调用代码方面没有其他同步或协调的情况下,那么它是线程安全的。

在我们的上下文中:

如果一个线程更改了实例的值,则所有线程都可以访问更改后的值,同时只有一个线程可以更改该值。

何时使用 atomic

如果实例变量将在多线程环境中被访问,则应使用 atomic

Atomic 的含义:

atomic 不如 nonatomic 快,因为 nonatomic 不需要运行时进行任何监视工作。

何时使用nonatomic

如果实例变量不会被多个线程更改,则可以使用nonatomic。这会提高性能。


5
你在这里说的一切都是正确的,但最后一句话基本上是"错误的",对于今天的编程来说这样做实在难以想象。你竟然会费心尝试用这种方式来"提高性能",这真是不可思议。(我的意思是,在接近此时之前,你将“不使用ARC”,“不使用NSString因为它很慢!”等等)。举个极端的例子,这就像是说“团队,不要在代码中放任何注释,因为这会拖慢我们的速度。”在没有牺牲可靠性的情况下,不存在任何现实的开发流程能够希望获得(不存在的)理论性能收益。 - Fattie
3
@JoeBlow 这是一个可以在这里验证的事实:https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/EncapsulatingData/EncapsulatingData.html - Durai Amuthan.H
3
Durai,FWIW,那个链接直接反驳了你的“Atomic = thread safety”观点。苹果公司在文档中明确表示,“属性的原子性并不等同于对象的线程安全性。”在实践中,仅使用原子操作通常不足以实现线程安全。 - Rob

79

在阅读了许多文章、Stack Overflow帖子并制作演示应用程序以检查变量属性特性后,我决定将所有属性信息汇总:

  1. atomic // 默认值
  2. nonatomic
  3. strong = retain // 默认值
  4. weak = unsafe_unretained
  5. retain
  6. assign // 默认值
  7. unsafe_unretained
  8. copy
  9. readonly
  10. readwrite // 默认值

在文章iOS中的变量属性特性或修饰符中,您可以找到上述所有属性,并且这肯定会有所帮助。

  1. atomic

    • atomic表示只有一个线程访问该变量(静态类型)。
    • atomic是线程安全的。
    • 但它在性能方面比较慢。
    • atomic是默认行为。
    • 在非垃圾收集环境中(即使用retain/release/autorelease时),原子访问器将使用锁来确保另一个线程不会干扰正确的值设置/获取。
    • 它实际上不是关键字。

    示例:

    @property (retain) NSString *name;

    @synthesize name;
  • nonatomic

    • nonatomic表示变量可以被多个线程访问(动态类型)。
    • nonatomic不保证线程安全。
    • 但是它在性能方面很快。
    • nonatomic不是默认行为。我们需要在属性中添加nonatomic关键字。
    • 当两个不同的进程(线程)同时访问同一个变量时,可能会导致意外的行为。

    示例:

  •     @property (nonatomic, retain) NSString *name;
    
        @synthesize name;
    

    分配和强引用/保留两者如何都可以是默认值? - BangOperator
    强引用(strong)是与 ARC 一起使用的,在 ARC 之前,retain 是默认选项。 - abdullahselek

    70
    我在这里找到了一个关于原子属性和非原子属性的解释(链接),这里有一些相关的内容:

    'atomic' 意味着其不能被拆分。 在操作系统/编程术语中,原子函数调用是指无法被中断的函数 - 整个函数必须被执行,并且在完成之前不能被操作系统的常规上下文切换交换出CPU。只是以防万一您不知道:由于CPU一次只能做一件事情,所以操作系统会在小的时间片内轮流让所有运行中的进程访问CPU,以给予多任务的幻觉。CPU调度程序可以(并且确实)在执行过程的任何时候中断进程 - 甚至在函数调用中途。因此,对于像更新共享计数器变量这样的操作,如果两个进程尝试同时更新变量,则它们必须以“原子方式”执行,即每个更新操作必须在任何其他进程可以被交换到CPU之前全部完成。

    因此,我猜在这种情况下,“原子”意味着属性读取方法无法被中断 - 实际上意味着被方法读取的变量不能在中途改变其值,因为某些其他线程/调用/函数被交换到CPU上。

    atomic变量由于不能被中断,所以在任何时候都保证它们所包含的值是未被破坏的(线程锁),但是确保此线程锁会使访问它们变慢。non-atomic变量则没有这样的保证,但提供了更快的访问速度。总之,当您知道您的变量不会被多个线程同时访问并且需要加快速度时,请使用non-atomic

    1
    链接已损坏。 ;( - Rob
    1
    这就是链接的问题 :( 幸运的是,我在我的答案中引用了相关的文本。 - tipycalFlow

    63

    原子性 :

    原子性保证对属性的访问将以原子方式进行。例如,它总是返回一个完全初始化的对象,在一个线程上对属性的任何get/set必须在另一个线程可以访问它之前完成。

    如果你想象以下函数同时在两个线程上发生,你会明白为什么结果不会很好。

    -(void) setName:(NSString*)string
    {
      if (name)
      {
        [name release]; 
        // what happens if the second thread jumps in now !?
        // name may be deleted, but our 'name' variable is still set!
        name = nil;
      }
    
      ...
    }
    

    优点: 每次返回完全初始化的对象,使其成为多线程情况下的最佳选择。

    缺点: 性能受损,使执行速度稍微慢一些。

    非原子性:

    与原子性不同,它不能确保每次返回完全初始化的对象。

    优点: 执行速度极快。

    缺点: 在多线程情况下存在垃圾值的可能性。


    5
    这个评论意义不是很清楚,你能解释一下吗?如果你在苹果网站上看例子,那么 atomic 关键字会在更新其属性时对对象进行同步。 - Andrew Grant

    53
    首先回答最简单的问题:您的后两个示例没有区别。默认情况下,属性访问器是原子的。
    在非垃圾回收环境中(即使用retain/release/autorelease时),原子访问器将使用锁来确保另一个线程不会干扰正确设置/获取值。
    请参阅苹果的Objective-C 2.0文档中的“性能和线程”部分,了解更多信息以及在创建多线程应用程序时要考虑的其他因素。

    8
    两个原因。首先,对于合成代码,它生成更快的代码(但不是线程安全的代码)。其次,如果您正在编写不是原子访问器的客户访问器,它允许您为任何将来的用户注释代码的接口,告诉他们代码在读取时不是原子性的,而无需让他们了解实现细节。 - Louis Gerbarg

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