4

根据我之前在 SO 中的讨论(请参阅对可以多次使用的对象的并发性的怀疑,如格式化程序),在这里我提出了一个更理论的问题,关于在应用程序生命周期内创建一次(并且从未修改过的对象,因此是只读的)并且可以从不同的线程访问它们。一个简单的用例是核心数据。格式化程序可用于不同的线程(主线程、导入线程等)。

NSFormatter例如,创建 s 的成本非常高。基于此,它们可以创建一次然后重复使用。可以遵循的典型模式(也由 NSFormatter 文章中的@mattt 突出显示如下。

+ (NSNumberFormatter *)numberFormatter {
    static NSNumberFormatter *_numberFormatter = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _numberFormatter = [[NSNumberFormatter alloc] init];
        [_numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
    });

    return _numberFormatter;
}

即使我确定这是一种非常好的方法(创建了一种只读/不可变对象),格式化程序也不是线程安全的,因此以线程安全的方式使用它们可能是危险的。当作者注意到可能发生崩溃的不同线程中使用时,我发现了关于 NSDateFormatter 崩溃中的论点的讨论。

NSDateFormatters 不是线程安全的;有一个后台线程试图同时使用相同的格式化程序(因此是随机性的)。

那么,从不同线程访问格式化程序可能会出现什么问题?有什么安全的模式可以遵循吗?

4

2 回答 2

7

格式化程序的具体答案:

在 iOS 7/OSX 10.9 之前,即使是对格式化程序的只读访问也不是线程安全的。ICU 有大量的惰性计算来响应请求,如果同时完成,可能会崩溃或产生不正确的结果。

在 iOS 7/OSX 10.9 中,NSDateFormatter并在NSNumberFormatter内部使用锁来序列化对底层 ICU 代码的访问,从而防止出现此问题。

一般回答:

仅真实访问/不可变对象通常确实是线程安全的,但很难分辨出哪些东西实际上是内部不可变的,哪些只是向外部世界呈现一个不可变的接口。

在您自己的代码中,您可以知道这一点。使用其他人的课程时,您必须依赖他们关于如何安全使用他们的课程的文档。

(编辑,因为请求序列化访问格式化程序的示例)

// in the dispatch_once where you create the formatter
dispatch_queue_t formatterQueue = dispatch_queue_create("date formatter queue", 0);

// where you use the formatter
dispatch_sync(formatterQueue, ^{ (do whatever you wanted to do with the formatter) });
于 2013-11-13T17:35:43.190 回答
3

我在问一个关于在应用程序生命周期内创建一次(并且从未修改过,因此是只读的)的对象的更多理论问题

这在技术上是不正确的:为了让一个对象真正是只读的,它也必须是不可变的。例如,可以创建NSMutableArray一次,但由于对象在创建后允许更改,因此不能将其视为只读,因此在没有额外同步的情况下并发使用是不安全的。

此外,逻辑上不可变的对象可能具有可变实现,使它们成为非线程安全的。例如,在第一次使用时执行延迟初始化的对象,或在其实例变量中缓存状态而不同步的对象,都不是线程安全的。似乎NSDateFormatter就是这样一个类:即使您没有调用方法来改变类的状态,您也可能会崩溃。

一种解决方案可能是使用线程本地存储:不是为NSDateFormatter每个应用程序创建一个,而是为每个线程创建一个,但这也可以让您节省一些 CPU 周期:一份报告提到他们设法减少了 5..10使用这个简单的技巧可以减少他们的启动时间。

于 2013-11-13T17:40:11.373 回答