原始值类型属性的线程安全 - Objective C

Posted

技术标签:

【中文标题】原始值类型属性的线程安全 - Objective C【英文标题】:Thread safety of primitive value type properties - Objective C 【发布时间】:2017-05-27 23:33:34 【问题描述】:

问题

我正在从事一个关注对象属性的线程安全性的项目。我知道,当属性是 NSString 之类的对象时,我可能会遇到多个线程同时读取和写入的情况。在这种情况下,您可能会收到损坏的读取,并且应用程序将崩溃或导致数据损坏。

我的问题是针对原始值类型属性,例如 BOOLs 或 NSIntegers。我想知道我是否会遇到类似的情况,即在从多个线程读取和写入时读取损坏的值(并且应用程序将崩溃)?无论哪种情况,我都对为什么感兴趣。

澄清 - 2017 年 1 月 13 日

我最感兴趣的是,原始值类型属性是否因多个线程同时访问而导致崩溃的可能性与 NSMutableString、自定义创建的对象等对象不同。此外,如果存在差异相对于多线程访问堆栈与堆上的内存时。

澄清 - 2017 年 12 月 1 日

感谢@Rob 在这里为我指出答案:***.com/a/34386935/1271826!这个答案有一个很好的例子,表明根据您所使用的架构类型(32 位与 64 位),使用原始属性时可能会得到未定义的结果。

虽然这是朝着回答我的问题迈出的一大步,但我仍然想知道两件事:

在访问堆栈和堆上的原始值属性时是否存在多线程差异(如我之前的说明中所述)? 如果您将程序限制为在一种架构上运行,您是否仍会在访问原始值属性时发现自己处于不设防状态?为什么?

我应该注意到,在回答这个问题时,这里有很多关于原子与非原子的对话。虽然这通常是一个重要的概念,但这个问题与通过使用原子属性修饰符或任何其他线程安全方法(例如使用 GCD)来防止未定义的多线程行为几乎没有关系。

【问题讨论】:

使用原子属性可以避免任何读取“损坏”值的机会。但这并不一定能避免读取“不正确”的值。 是的,我熟悉使用原子与非原子,但是读取原始值类型属性的“损坏”值是否可能?如果有,为什么? 是的,有可能。如果您在 32 位目标上运行 ***.com/a/34386935/1271826 处的代码,您将看到损坏的值。 【参考方案1】:

如果您的原始值类型属性是atomic,那么您可以确保它不会被损坏,因为您从一个线程读取它同时从另一个线程设置它(只要您只使用访问器方法,而不是与直接支持 ivar)。这就是atomic 的全部目的。而且,正如您所建议的,这仅适用于基本数据类型(或既不可变又无状态的对象)。但在这些狭隘的情况下,atomic 会很有用。

话虽如此,但这与得出应用程序是线程安全的结论相去甚远。它只向您保证对该属性的访问是线程安全的。但通常必须在更广泛的背景下考虑线程安全。 (我知道您向我们保证,这里不是这种情况,但我为那些过早得出结论 atomic 足以实现线程安全的未来读者提供了这一点。通常情况并非如此。)

例如,如果您的NSInteger 属性是“此缓存对象中有多少项”,那么NSInteger 不仅必须具有其访问权限synchronized,而且还必须与所有同步与缓存对象的交互(例如“将项目添加到缓存”和“从缓存中删除项目”任务)。而且,在这些情况下,由于您将以某种方式与这个更广泛的对象同步所有交互(例如,使用 GCD 队列、锁、@synchronized 指令等),因此使NSInteger 属性atomic 变得多余,因此适度减少高效。

归根结底,在有限的情况下,atomic 可以为基本数据类型提供线程安全,但从更广泛的背景来看,它通常是不够的。


你后来说你不关心比赛条件。对于它的价值,苹果认为不存在良性竞争。请参阅 WWDC 2016 视频 Thread Sanitizer and Static Analysis(大约 14:40 开始)。

无论如何,您建议您只关心值是否会损坏或应用程序是否会崩溃:

我想知道我是否会遇到类似的情况,即在从多个线程读取和写入时读取损坏的值(并且应用程序会崩溃)?

底线是,如果您从一个线程读取数据同时在另一个线程上发生变异,那么行为就是未定义的。它可能会有所不同。建议您避免这种情况。

在实践中,它是目标架构的一个功能。例如,在 32 位 x86 目标上的 64 位类型(例如long long)上,您可以轻松检索损坏的值,其中一半的 64 位值已设置,而另一半未设置。 (例如,参见https://***.com/a/34386935/1271826。)在处理原始类型时,这只会导致无意义的无效数值。对于指向对象的指针,这显然会产生灾难性的影响。

但是,即使您处于一个没有问题的环境中,它也是一种非常脆弱的方法来避免同步以实现线程安全。当在新的、意想不到的硬件架构上运行或在不同的配置下编译时,它很容易崩溃。我鼓励您观看 Thread Sanitizer and Static Analysis 视频以了解更多信息。

【讨论】:

感谢您的回复。在您的第一段中,您是说诸如 NSMutableStrings 之类的可变对象不能被多线程破坏吗?如果是这样,他们绝对可以。我提出的问题集中在被多线程破坏的原始值类型。另外,你能解释一下为什么原语可以或不能被多线程破坏吗?我意识到atomic 的目的是防止损坏,但是易受多线程影响的原语是否与对象相同? 首先,我对对象的资格说“不可变和无状态”,而不是“可变”(从技术上讲,即使这也不完全正确,但你明白了)。其次,atomic 原始属性不能被破坏,因为合成的访问器方法将同步所有访问(例如,如果您当前正在一个线程上读取,则另一个线程上的写入将等到读取完成,反之亦然)。 最后,我不同意您的最后一个问题,“对多线程敏感的原语是否与对象一样?”保证您了解atomic 的作用。 atomic 将所有访问器方法同步到属性,因此根据定义,这意味着只要您只使用属性的访问器方法,就不能通过多个线程破坏原始属性。对于对象,atomic 仅仅保证了指向对象的指针的完整性,但显然不能保证对象本身,这就是为什么可变对象在多线程代码中通常是一个问题。 感谢您提供更多信息。我认为断开连接是因为我不关心 atomic 属性是如何工作的。我感兴趣的是,原始值类型属性是否由于多个线程同时访问它而容易崩溃,而不是像 NSMutableString、自定义创建的对象等对象。此外,如果在访问内存时存在差异相对于多线程的堆栈与堆。我将把这个澄清添加到我原来的问题中。很抱歉有任何混淆。 您想知道原始类型属性是否“由于多个线程同时访问而容易崩溃”。不,当您拥有atomic 时,该内存已同步,因此它不能同时被访问。因此,例如,如果您有NSInteger localVar = self.intProperty,则localVar 基本上(没有双关语)是不同内存地址的本地副本。因此,不可能同时访问支持该原始属性的 ivar 的内存(只要您不取消引用它,直接访问 ivar 等)。

以上是关于原始值类型属性的线程安全 - Objective C的主要内容,如果未能解决你的问题,请参考以下文章

Objective-c 中 SEL 类型的属性

在 Swift 3 的值类型中访问 Objective-C 类别的属性(关联参考)

在 Swift 3 的值类型中访问 Objective-C 类别的属性(关联参考)

Objective-C中的@Property具体解释

在objective-c中为原始类型分配内存的最佳实践

同时覆盖具有相同值的变量是不是安全?