修改完成处理程序中的可变对象

Sea*_*Lee 6 thread-safety gamekit ios objective-c-blocks

我有一个关于Apple的以下代码示例的线程安全性的问题(来自GameKit编程指南)

这是从游戏中心加载成就并在本地保存:

步骤1)将一个可变字典属性添加到报告成就的类中.该词典存储成就对象的集合.

@property(nonatomic, retain) NSMutableDictionary *achievementsDictionary;
Run Code Online (Sandbox Code Playgroud)

步骤2)初始化成就字典.

achievementsDictionary = [[NSMutableDictionary alloc] init];
Run Code Online (Sandbox Code Playgroud)

步骤3)修改加载加载成就数据的代码,将成就对象添加到字典中.

{
    [GKAchievement loadAchievementsWithCompletionHandler:^(NSArray *achievements, NSError *error)
        {
            if (error == nil)
            {
                for (GKAchievement* achievement in achievements)
                    [achievementsDictionary setObject: achievement forKey: achievement.identifier];
            }
        }];
Run Code Online (Sandbox Code Playgroud)

我的问题如下 - achievementDictionary对象正在完成处理程序中被修改,没有任何排序锁.这是否允许,因为完成处理程序是一个工作块,iOS将保证在主线程上作为单元执行?并且永远不会遇到线程安全问题?

在另一个Apple示例代码(GKTapper)中,此部分的处理方式不同:

@property (retain) NSMutableDictionary* earnedAchievementCache; // note this is atomic
Run Code Online (Sandbox Code Playgroud)

然后在处理程序中:

[GKAchievement loadAchievementsWithCompletionHandler: ^(NSArray *scores, NSError *error)
        {
            if(error == NULL)
            {
                NSMutableDictionary* tempCache= [NSMutableDictionary dictionaryWithCapacity: [scores count]];
                for (GKAchievement* score in scores)
                {
                    [tempCache setObject: score forKey: score.identifier];
                }
                self.earnedAchievementCache= tempCache;
            }
        }];
Run Code Online (Sandbox Code Playgroud)

那么为什么不同的风格,并且比另一种方式更正确?

Jer*_*man 2

是否允许这样做,因为完成处理程序是 iOS 保证在主线程上作为单元执行的一个工作块?并且永远不会遇到线程安全问题?

这里绝对不是这种情况。的文档-loadAchievementsWithCompletionHandler:明确警告完成处理程序可能会在启动加载的线程以外的线程上调用。

Apple 的“线程编程指南”NSMutableDictionary对线程不安全类进行了分类,但对此进行了限定:“在大多数情况下,只要一次仅从一个线程使用它们,就可以从任何线程使用这些类。”

因此,如果两个应用程序的设计使得在工作任务完成更新之前不会对成就缓存进行任何操作,则不需要同步。这是我认为第一个例子是安全的唯一方法,而且这是一个脆弱的安全性。

后一个示例看起来依赖于原子属性支持来在旧缓存和新缓存之间进行切换。这应该是安全的,前提是对该属性的所有访问都是通过其访问器而不是直接 ivar 访问。这是因为访问器彼此同步,因此您不会冒看到半集值的风险。此外,getter 保留并自动释放返回的值,因此旧版本的代码将能够完成使用它而不会崩溃,因为它是在工作中释放的。非原子 getter 只是直接返回对象,这意味着如果另一个线程为该属性设置了新值,则可以从代码中释放该对象。直接 ivar 访问可能会遇到同样的问题。

我想说后一个例子既正确又优雅,尽管可能有点过于微妙,没有注释解释属性的原子性是多么重要。