Изменение изменяемого объекта в обработчике завершения

У меня есть вопрос о безопасности потоков в следующем примере кода от 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];
            }
        }];

Мой вопрос заключается в следующем: в обработчике завершения модифицируется объект achievementDictionary без каких-либо блокировок сортировки. Разрешено ли это, потому что обработчики завершения — это блок работы, который будет гарантировано 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;
            }
        }];

Так почему же другой стиль, и является ли один способ более правильным, чем другой?


person Sean S Lee    schedule 16.08.2011    source источник


Ответы (1)


Разрешено ли это, потому что обработчики завершения — это блок работы, который будет гарантировано iOS выполняться как единица в основном потоке? И никогда не сталкиваться с проблемами безопасности потоков?

Это определенно не тот случай здесь. Документация для -loadAchievementsWithCompletionHandler: явно предупреждает, что обработчик завершения может быть вызван в потоке, отличном от того, с которого вы начали загрузку.

«Руководство по программированию потоков» от Apple классифицирует NSMutableDictionary как классы, небезопасные для потоков, но уточняет это следующим образом: «В большинстве случаев вы можете использовать эти классы из любого потока, если вы используете их только из одного потока за раз».

Таким образом, если оба приложения разработаны таким образом, что ничего не будет работать с кешем достижений, пока рабочая задача не завершит его обновление, то синхронизация не потребуется. Это единственный способ, которым я могу считать первый пример безопасным, и это ненадежная безопасность.

Последний пример выглядит так, как будто он полагается на поддержку атомарных свойств для переключения между старым кешем и новым кешем. Это должно быть безопасно, при условии, что весь доступ к свойству осуществляется через его методы доступа, а не через прямой доступ к ivar. Это связано с тем, что методы доступа синхронизированы по отношению друг к другу, поэтому вы не рискуете увидеть половинчатое значение. Также геттер сохраняет и автоматически выпускает возвращаемое значение, так что код со старой версией сможет закончить работу с ним без сбоев, потому что он был выпущен в середине своей работы. Неатомарный геттер просто возвращает объект напрямую, что означает, что он может быть освобожден из-под вашего кода, если для этого свойства было установлено новое значение другим потоком. Прямой доступ к ivar может столкнуться с той же проблемой.

Я бы сказал, что последний пример является одновременно правильным и элегантным, хотя, возможно, слишком тонким без комментариев, объясняющих, насколько важна атомарность свойства.

person Jeremy W. Sherman    schedule 05.09.2011
comment
спасибо Джереми. я также задавал вопросы об обработчиках завершения блока в целом. Если в документации явно не указано, где вызывается обработчик завершения, можно ли предположить, что он не вызывается в основном потоке? Если указано, что он вызывается из основной очереди, то безопасно ли делать что-либо внутри, не беспокоясь о безопасности потоков (при условии, что весь доступ осуществляется из основного потока или компл. обработчика, вызываемого в основной очереди). - person Sean S Lee; 06.09.2011
comment
Если в нем не указано, что ваш блок будет вызываться в основной очереди/потоке, вам придется принять собственные меры, чтобы убедиться, что это так, если вам это нужно. На самом деле это может быть вызвано в основном потоке, но в этот момент этот факт является деталью реализации, а не частью контракта API, и может измениться без предупреждения. Если он указан для вызова из основной очереди, вы можете в полной мере воспользоваться этим. - person Jeremy W. Sherman; 06.09.2011
comment
Тогда последний вопрос: если я изменю первый обработчик завершения на использование dispatch_async(dispatch_get_main_queue(), ^{ for (достижение GKAchievement* в достижениях) [achievementsDictionary setObject:chievement forKey:chievement.identifier]; });) будет ли этот код потокобезопасным также? Я помню, что читал что-то о том, что это небезопасно. - person Sean S Lee; 06.09.2011
comment
Threadsafe — это не просто бинарное качество. Это сильно зависит от контекста. Это одна из причин, почему так легко испортить многопоточные приложения. Если вы можете гарантировать, что весь доступ к вашему achievementsDictionary происходит только в основном потоке, то форсирование этих изменений в основном потоке сериализует их относительно всех других доступов, поэтому все должно быть в порядке. - person Jeremy W. Sherman; 07.09.2011
comment
Спасибо за терпеливость! я думаю, что понимаю это лучше сейчас. - person Sean S Lee; 07.09.2011