Что означает Apple, когда говорят, что NSManagedObjectContext принадлежит потоку или очереди, которые его создали?

Кажется, что в ноябре Apple обновила и NSManagedObjectContext Reference Reference и Руководство по программированию основных данных", чтобы явно благословлять серийные очереди GCD и NSOperationQueues в качестве приемлемых механизмов для синхронизации доступа к NSManagedObjectContext. Но их советы кажутся двусмысленными и, возможно, противоречивыми, и я хочу убедиться, что правильно понял.

Раньше общепринятая мудрость заключалась в том, что NSManagedObjectContext можно было получить только из потока, который ее создал, и что использование последовательной очереди для синхронизации было недостаточным; хотя последовательные очереди выполняют только одну операцию за раз, эти операции могут быть запланированы на разных потоках, а MOC это не нравится.

Но теперь из руководства по программированию мы имеем:

Вы можете использовать потоки, очереди очередей или очереди отправки для concurrency. Для краткости в этой статье используется "поток" для ссылки на любой из них.

Пока, так хорошо (хотя их объединение потоков и очередей бесполезно). Таким образом, я могу безопасно использовать единый контекст для (последовательной) очереди, а не один за операцию/блок, правильно? Apple даже имеет наглядное представление об этом в сессиях основных данных WWDC.

Но... где вы создаете контекст для очереди? В документации NSManagedObjectContext состояние Apple:

[Контекст] предполагает, что владельцем по умолчанию является поток или очередь, которые его выделяют, - это определяется потоком, который вызывает его метод init. Поэтому вы не должны инициализировать контекст в одном потоке, а затем передавать его в другой поток.

Итак, теперь у нас есть идея NSManagedObjectContext, которая должна знать, кто ее владелец. Я предполагаю, что это означает, что первая операция, которая должна быть выполнена в очереди, должна создать MOC и сохранить ссылку на нее для остальных операций, которые будут использоваться.

Правильно ли это? Единственная причина, по которой я не решаюсь, состоит в том, что в статье NSManagedObjectContext говорится:

Вместо этого вы должны передать ссылку на постоянный координатор хранилища и получить принимающий поток/очередь для создания нового контекста, полученного из этого. Если вы используете NSOperation, вы должны создать контекст в основном (для последовательной очереди) или запустить (для параллельной очереди).

Теперь Apple, похоже, объединяет операции с очередями, которые планируют их выполнение. Это делает мою голову и заставляет меня задаться вопросом, действительно ли они хотят, чтобы вы просто создали новый MOC для каждой операции. Что мне не хватает?

Ответ 1

NSManagedObjectContext и любые связанные с ним управляемые объекты должны быть привязаны к одному игроку (поток, сериализованная очередь, NSOperationQueue с max concurrency= 1).

Этот шаблон называется ограничением или изолированием потоков. Не существует большой фразы для (thread || serialized queue || NSOperationQueue с max concurrency= 1), поэтому в документации далее говорится: "Мы будем использовать" поток "для остальной части документа Core Data, когда мы подразумеваем любой из этих трех способов получения сериализованного потока управления"

Если вы создаете MOC в одном потоке, а затем используете его на другом, вы нарушили ограничение потока, подвергая ссылку объекта MOC двум потокам. Просто. Не делай этого. Не пересекайте потоки.

Мы явно вызываем NSOperation, потому что, в отличие от потоков и GCD, у этой проблемы есть такая странная проблема, когда -init работает в потоке, создающем NSOperation, но -main работает в потоке, выполняющем NSOperation. Это имеет смысл, если вы правильно оцениваете это, но это не интуитивно. Если вы создадите свой MOC в [NSOperation init], то NSOperation будет эффективно нарушать ограничение потока, прежде чем ваш метод -main даже будет запущен, и вы будете закрыты.

Мы активно препятствуем/отказываемся от использования MOC и потоков любым другим способом. Хотя теоретически можно делать то, что упоминается в bbum, никто так и не понял. Все споткнулись, забыли нужный призыв к -lock в 1-м месте, "init запускается где?" Или иным образом вычистил себя. С пулами автозаполнения и циклом событий приложения и менеджером отмены и привязками cocoa и KVO существует только так много способов, чтобы один поток поддерживал ссылку на MOC после того, как вы попытались передать его в другом месте. Это гораздо сложнее, чем даже продвинутые разработчики cocoa, пока не начнут отлаживать. Так что не очень полезный API.

Документация изменилась, чтобы уточнить и подчеркнуть шаблон ограничения потока в качестве единственного нормального пути. Вам следует подумать о том, чтобы попытаться быть экстравагантным, используя -lock и -unlock в NSManagedObjectContext, чтобы быть (a) невозможным и (b) де-факто устаревшим. Это не буквально не рекомендуется, потому что код работает так же хорошо, как и когда-либо. Но ваш код использует его неправильно.

Некоторые люди создали MOC на 1 поток и передали их другому, не вызывая -lock. Это никогда не было законным. Нить, которая создала MOC, всегда была владельцем MOC по умолчанию. Это стало более частой проблемой для MOC, созданных в основном потоке. Основной поток MOC взаимодействуют с основным циклом событий приложения для отмены, управления памятью и некоторыми другими причинами. На 10.6 и iOS 3, MOCs принимают более активное преимущество в том, что они принадлежат основному потоку.

Хотя очереди не привязаны к конкретным потокам, если вы создаете MOC в контексте очереди, все будет правильно. Ваша обязанность - следовать публичному API.

Если очередь сериализована, вы можете совместно использовать MOC с последующими блоками, которые выполняются в этой очереди.

Поэтому не подвергайте NSManagedObjectContext * более чем одному потоку (актеру и т.д.) ни при каких обстоятельствах. Существует одна двусмысленность. Вы можете передать NSNotification * из уведомления didSave в другой поток MOC -mergeChangesFromContextDidSaveNotification: method.

  • Бен

Ответ 2

Похоже, вы все правильно. Если вы используете потоки, поток, который хочет контекст, должен его создать. Если вы используете очереди, очередь, которая хочет контекст, должна создать ее, скорее всего, как первый блок для выполнения в очереди. Похоже, единственная запутанная часть - это бит о NSOperations. Я думаю, что путаница в NSOperations не дает никаких гарантий относительно того, какой основной поток/очередь они запускают, поэтому небезопасно делиться MOC между операциями, даже если они все работают на одном NSOperationQueue. Альтернативное объяснение заключается в том, что это просто запутывает документацию.

Подводя итог:

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

Изменить: Согласно bbum, единственным реальным требованием является необходимость сериализации доступа. Это означает, что вы можете совместно использовать MOC через NSOperations, пока все операции добавляются в одну очередь, а очередь не позволяет выполнять одновременные операции.