【问题标题】:Modifying mutable object in completion handler在完成处理程序中修改可变对象
【发布时间】:2011-08-16 22:08:14
【问题描述】:

我对来自 Apple 的以下代码示例的线程安全有疑问(来自 GameKit 编程指南)

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

第 1 步)向您的类添加一个可变字典属性以报告成就。该字典存储成就对象的集合。

@property(nonatomic, retain) NSMutableDictionary *achievementsDictionary;

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

achievementsDictionary = [[NSMutableDictionary alloc] init];

第 3 步)修改加载成就数据的代码以将成就对象添加到字典中。

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

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

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

@property (retain) NSMutableDictionary* earnedAchievementCache; // note this is atomic

然后在处理程序中:

[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;
            }
        }];

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

【问题讨论】:

    标签: ios thread-safety gamekit objective-c-blocks


    【解决方案1】:

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

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

    Apple 的“线程编程指南”将 NSMutableDictionary 归类为线程不安全的类,但将其限定为:“在大多数情况下,您可以在任何线程中使用这些类,只要您一次只能从一个线程中使用它们。”

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

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

    我会说后一个例子既正确又优雅,虽然可能有点过于微妙而没有注释来解释属性的原子性有多重要。

    【讨论】:

    • 感谢杰里米。我也在质疑一般的块完成处理程序。如果文档没有明确说明调用完成处理程序的位置,那么假设它没有在主线程中调用是否安全?如果指定从主队列调用,那么在内部做任何事情而不用担心线程安全是否安全(假设所有访问都来自主线程或主队列上调用的compl.handler)。
    • 如果它没有指定你的块将在主队列/线程上被调用,那么你必须采取自己的措施来确保它是,如果你需要的话。它实际上可能在主线程上被调用,但此时,该事实是一个实现细节,而不是 API 合同的一部分,并且可能会在没有警告的情况下更改。如果指定从主队列调用,那么您可以充分利用它。
    • 最后一个问题 - 如果我将第一个完成处理程序更改为使用 dispatch_async(dispatch_get_main_queue(), ^{ for (GKAchievement* 成就中的成就) [achievementsDictionary setObject: achievement forKey: achievement.identifier]; } );) 这段代码也是线程安全的吗?我记得读过一些关于这不安全的东西..
    • "Threadsafe" 不是简单的二进制质量。它在很大程度上取决于上下文。这就是为什么很容易搞砸多线程应用程序的部分原因。如果您可以保证对 achievementsDictionary 的所有访问仅发生在主线程上,那么强制对主线程的这些更改会相对于所有其他访问将它们序列化,所以您应该没问题。
    • 感谢您的耐心等待!我想我现在更好地理解了这一点。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-03-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-03
    • 2018-11-06
    • 2015-08-30
    相关资源
    最近更新 更多