Если вы разрабатываете маленькое или простое приложение, вы, вероятно, не увидите преимущества выполнения операций с основными данными в фоновом режиме. Однако что произойдет, если вы импортировали сотни или тысячи записей в основной поток при первом запуске приложения? Последствия могут быть драматичными. Например, ваше приложение может быть убито сторожевым таймером Apple из-за слишком долгого запуска.
В этой статье мы рассмотрим опасности, возникающие при использовании Core Data в нескольких потоках, и исследуем несколько решений для решения проблемы.
1. Безопасность ниток
При работе с Core Data важно всегда помнить, что Core Data не является поточно-ориентированным. Базовые данные ожидают запуска в одном потоке. Это не означает, что каждая операция Core Data должна выполняться в главном потоке, что верно для UIKit, но это означает, что вам нужно помнить, какие операции выполняются с какими потоками. Это также означает, что вам нужно быть осторожным, как изменения из одного потока распространяются на другие потоки.
Работа с базовыми данными в нескольких потоках на самом деле очень проста с теоретической точки зрения. NSManagedObject
, NSManagedObjectContext
и NSPersistentStoreCoordinator
не являются поточно- NSPersistentStoreCoordinator
. Доступ к экземплярам этих классов возможен только из потока, в котором они были созданы. Как вы можете себе представить, это становится немного сложнее на практике.
NSManagedObject
Мы уже знаем, что NSManagedObject
не является потокобезопасным, но как получить доступ к записи из разных потоков? Экземпляр NSManagedObject
имеет свойство objectID
которое возвращает экземпляр класса NSManagedObjectID
. Класс NSManagedObjectID
является потокобезопасным, и экземпляр этого класса содержит всю информацию, которая необходима контексту управляемого объекта для извлечения соответствующего управляемого объекта.
1
2
|
// Object ID Managed Object
let objectID = managedObject.objectID
|
В следующем фрагменте кода мы запрашиваем контекст управляемого объекта для управляемого объекта, который соответствует objectID
. objectWithID(_:)
и existingObjectWithID(_:)
возвращают локальную версию — локальную для текущего потока — соответствующего управляемого объекта.
01
02
03
04
05
06
07
08
09
10
11
|
// Fetch Managed Object
let managedObject = managedObjectContext.objectWithID(objectID)
// OR
do {
let managedObject = try managedObjectContext.existingObjectWithID(objectID)
} catch {
let fetchError = error as NSError
print(«\(fetchError), \(fetchError.userInfo)»)
}
|
Основное правило, которое нужно запомнить, — не передавать экземпляр NSManagedObject
из одного потока в другой. Вместо этого передайте objectID
управляемого объекта и запросите контекст управляемого объекта потока для локальной версии записи.
NSManagedObjectContext
Поскольку класс NSManagedObjectContext
не является потокобезопасным, мы могли бы создать контекст управляемого объекта для каждого потока, взаимодействующего с базовыми данными. Эта стратегия часто упоминается как ограничение потока .
Распространенным подходом является сохранение контекста управляемого объекта в словаре потока, словаре для хранения данных, специфичных для потока. Посмотрите на следующий пример, чтобы увидеть, как это работает на практике.
1
2
3
|
// Add Object to Thread Dictionary
let currentThread = NSThread.currentThread()
currentThread.threadDictionary.setObject(managedObjectContext, forKey: «managedObjectContext»)
|
Не так давно Apple рекомендовала такой подход. Даже при том, что это работает хорошо, есть еще один и лучший вариант, который Apple рекомендует сегодня. Мы рассмотрим этот вариант через несколько минут.
NSPersistentStoreCoordinator
Как насчет постоянного координатора магазина? Вам нужно создать отдельный постоянный координатор хранилища для каждого потока. Хотя это возможно и является одной из стратегий, которые Apple рекомендовала, в этом нет необходимости.
Класс NSPersistentStoreCoordinator
был разработан для поддержки нескольких контекстов управляемых объектов, даже если эти контексты управляемых объектов создавались в разных потоках. Поскольку класс NSManagedObjectContext
блокирует координатор постоянного хранилища при доступе к нему, несколько контекстов управляемого объекта могут использовать один и тот же координатор постоянного хранилища, даже если эти контексты управляемого объекта живут в разных потоках. Это делает многопоточную настройку Core Data намного более управляемой и менее сложной.
2. Стратегии параллелизма
Итак, мы узнали, что вам нужно несколько контекстов управляемых объектов, если вы выполняете операции Core Data в нескольких потоках. Однако предостережение заключается в том, что контексты управляемого объекта не знают о существовании друг друга. Изменения, вносимые в управляемый объект в одном контексте управляемого объекта, не распространяются автоматически в другие контексты управляемого объекта. Как мы решаем эту проблему?
Apple рекомендует использовать две популярные стратегии: уведомления и контексты управляемых объектов «родитель-потомок». Давайте посмотрим на каждую стратегию и выясним их плюсы и минусы.
Сценарий, который мы возьмем в качестве примера, это подкласс NSOperation
который выполняет работу в фоновом режиме и получает доступ к базовым данным в фоновом потоке операции. Этот пример покажет вам различия и преимущества каждой стратегии.
Стратегия 1: Уведомления
Ранее в этой серии я познакомил вас с классом NSFetchedResultsController
и вы узнали, что контекст управляемого объекта NSFetchedResultsController
три типа уведомлений:
-
NSManagedObjectContextObjectsDidChangeNotification
: это уведомление публикуется, когда один из управляемых объектов контекста управляемого объекта изменился. -
NSManagedObjectContextWillSaveNotification
: это уведомление публикуется до того, как контекст управляемого объекта выполняет операцию сохранения. -
NSManagedObjectContextDidSaveNotification
: это уведомление публикуется после того, как контекст управляемого объекта выполняет операцию сохранения.
Когда контекст управляемого объекта сохраняет свои изменения в постоянном хранилище, через координатор постоянного хранилища, другие контексты управляемого объекта могут захотеть узнать об этих изменениях. Это очень легко сделать, и еще проще включить или объединить изменения в другой контекст управляемого объекта. Давайте поговорим о коде.
Мы создаем не параллельную операцию, которая выполняет некоторую работу в фоновом режиме и требует доступа к базовым данным. Вот как может выглядеть реализация подкласса NSOperation
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
import UIKit
import CoreData
class Operation: NSOperation {
let mainManagedObjectContext: NSManagedObjectContext
var privateManagedObjectContext: NSManagedObjectContext!
init(managedObjectContext: NSManagedObjectContext) {
mainManagedObjectContext = managedObjectContext
super.init()
}
override func main() {
// Initialize Managed Object Context
privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
// Configure Managed Object Context
privateManagedObjectContext.persistentStoreCoordinator = mainManagedObjectContext.persistentStoreCoordinator
// Add Observer
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(self, selector: «managedObjectContextDidSave:», name: NSManagedObjectContextDidSaveNotification, object: privateManagedObjectContext)
// Do Some Work
// …
if privateManagedObjectContext.hasChanges {
do {
try privateManagedObjectContext.save()
} catch {
// Error Handling
// …
}
}
}
}
|
Есть несколько важных деталей, которые необходимо уточнить. Мы инициализируем контекст закрытого управляемого объекта и устанавливаем его свойство координатора постоянного хранилища, используя объект mainManagedObjectContext
. Это прекрасно, потому что мы не mainManagedObjectContext
к mainManagedObjectContext
, мы только запрашиваем его для ссылки на постоянный координатор хранилища приложения. Мы не нарушаем правило ограничения потока.
Важно инициализировать контекст закрытого управляемого объекта в методе main()
операции, поскольку этот метод выполняется в фоновом потоке, в котором выполняется операция. Разве мы не можем инициализировать контекст управляемого объекта в методе операции init(managedObjectContext:)
? Ответ — нет. Метод init(managedObjectContext:)
операции выполняется в потоке, в котором инициализирован экземпляр Operation
, который, скорее всего, является основным потоком. Это противоречит цели частного контекста управляемого объекта.
В методе main()
Operation
мы добавляем экземпляр Operation
в качестве наблюдателя любых уведомлений NSManagedObjectContextDidSaveNotification
публикуемых контекстом закрытого управляемого объекта.
Затем мы выполняем работу, для которой была создана операция, и сохраняем изменения контекста частного управляемого объекта, который вызовет уведомление NSManagedObjectContextDidSaveNotification
. Давайте посмотрим, что происходит в методе managedObjectContextDidSave(_:)
.
1
2
3
4
5
6
7
|
// MARK: —
// MARK: Notification Handling
func managedObjectContextDidSave(notification: NSNotification) {
dispatch_async(dispatch_get_main_queue()) { () -> Void in
self.mainManagedObjectContext.mergeChangesFromContextDidSaveNotification(notification)
}
}
|
Как видите, его реализация короткая и простая. Мы вызываем mergeChangesFromContextDidSaveNotification(_:)
для основного контекста управляемого объекта, передавая объект уведомления. Как я упоминал ранее, уведомление содержит изменения, вставки, обновления и удаления контекста управляемого объекта, который опубликовал уведомление.
Это ключ для вызова этого метода в потоке, в котором был создан основной контекст управляемого объекта, в основном потоке. Вот почему мы отправляем этот вызов в очередь основного потока. Чтобы сделать это проще и прозрачнее, вы можете использовать performBlock(_:)
или performBlockAndWait(_:)
чтобы обеспечить слияние изменений в очереди контекста управляемого объекта. Подробнее об этих методах мы поговорим позже в этой статье.
1
2
3
4
5
6
7
|
// MARK: —
// MARK: Notification Handling
func managedObjectContextDidSave(notification: NSNotification) {
mainManagedObjectContext.performBlock { () -> Void in
self.mainManagedObjectContext.mergeChangesFromContextDidSaveNotification(notification)
}
}
|
Использование класса Operation
для использования так же просто, как инициализация экземпляра, передача контекста управляемого объекта и добавление операции в очередь операций.
1
2
3
4
5
|
// Initialize Import Operation
let operation = Operation(managedObjectContext: managedObjectContext)
// Add to Operation Queue
operationQueue.addOperation(operation)
|
Стратегия 2: Контексты управляемых родителем / ребенком объектов
Начиная с iOS 6, есть еще лучшая, более элегантная стратегия. Давайте вернемся к классу Operation
и используем контексты родительского / дочернего управляемого объекта. Концепция контекста управляемого объекта родитель / потомок проста, но мощна. Позвольте мне объяснить, как это работает.
Дочерний контекст управляемого объекта зависит от его родительского контекста управляемого объекта для сохранения его изменений в соответствующем постоянном хранилище. Фактически, дочерний контекст управляемого объекта не имеет доступа к постоянному координатору хранилища. Всякий раз, когда сохраняется контекст дочернего управляемого объекта, содержащиеся в нем изменения передаются в контекст родительского управляемого объекта. Нет необходимости использовать уведомления для ручного объединения изменений в основной или родительский контекст управляемого объекта.
Еще одним преимуществом является производительность. Поскольку у дочернего контекста управляемого объекта нет доступа к постоянному координатору хранилища, изменения не передаются последнему при сохранении контекста дочернего управляемого объекта. Вместо этого изменения помещаются в контекст родительского управляемого объекта, что делает его грязным. Изменения не передаются автоматически постоянному координатору хранилища.
Контексты управляемого объекта могут быть вложенными. Дочерний контекст управляемого объекта может иметь собственный дочерний контекст управляемого объекта. Применяются те же правила. Однако важно помнить, что изменения, передаваемые в контекст родительского управляемого объекта, не переносятся в другие контексты дочернего управляемого объекта. Если дочерний элемент A передает свои изменения своему родителю, то дочерний элемент B не знает об этих изменениях.
Создание контекста управляемого дочернего объекта лишь немного отличается от того, что мы видели до сих пор. Мы инициализируем дочерний контекст управляемого объекта, вызывая init(concurrencyType:)
. Тип параллелизма, который принимает инициализатор, определяет модель потоков контекста управляемого объекта. Давайте посмотрим на каждый тип параллелизма.
-
MainQueueConcurrencyType
: контекст управляемого объекта доступен только из основного потока. Исключение выдается, если вы пытаетесь получить к нему доступ из любого другого потока. -
PrivateQueueConcurrencyType
: при создании контекста управляемого объекта с типом параллелизмаPrivateQueueConcurrencyType
контекст управляемого объекта связывается с частной очередью, и к нему можно получить доступ только из этой частной очереди. -
ConfinementConcurrencyType
: это тип параллелизма, который соответствует концепции ограничения потока, которую мы исследовали ранее. Если вы создаете контекст управляемого объекта с помощьюinit()
, типом параллелизма этого контекста управляемого объекта являетсяConfinementConcurrencyType
. Apple отказалась от этого типа параллелизма с iOS 9. Это также означает, чтоinit()
устарела с iOS 9.
Существует два ключевых метода, которые были добавлены в платформу Core Data, когда Apple представила контексты родительского / дочернего управляемого объекта: performBlock(_:)
и performBlockAndWait(_:)
. Оба метода сделают вашу жизнь намного проще. Когда вы вызываете performBlock(_:)
в контексте управляемого объекта и передаете блок кода для выполнения, Core Data гарантирует, что блок выполняется в правильном потоке. В случае типа параллелизма PrivateQueueConcurrencyType
это означает, что блок выполняется в частной очереди этого контекста управляемого объекта.
Разница между performBlock(_:)
и performBlockAndWait(_:)
проста. Метод performBlock(_:)
не блокирует текущий поток. Он принимает блок, планирует его выполнение в правильной очереди и продолжает выполнение следующего оператора.
Однако performBlockAndWait(_:)
блокируется. Поток, из которого performBlockAndWait(_:)
ожидает завершения блока, переданного методу, перед выполнением следующего оператора. Преимущество заключается в том, что вложенные вызовы performBlockAndWait(_:)
выполняются по порядку.
Чтобы закончить эту статью, я хотел бы провести рефакторинг класса Operation
чтобы использовать преимущества контекста управляемого объекта родитель / потомок. Вы быстро заметите, что это значительно упрощает NSOperation
подкласс NSOperation
. Метод main()
меняется совсем немного. Посмотрите на его обновленную реализацию ниже.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
override func main() {
// Initialize Managed Object Context
privateManagedObjectContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
// Configure Managed Object Context
privateManagedObjectContext.parentContext = mainManagedObjectContext
// Do Some Work
// …
if privateManagedObjectContext.hasChanges {
do {
try privateManagedObjectContext.save()
} catch {
// Error Handling
// …
}
}
}
|
Вот и все. Основной контекст управляемого объекта является родителем частного контекста управляемого объекта. Обратите внимание, что мы не устанавливаем свойство persistentStoreCoordinator
контекста частного управляемого объекта и не добавляем операцию в качестве наблюдателя для уведомлений NSManagedObjectContextDidSaveNotification
. Когда закрытый контекст управляемого объекта сохраняется, изменения автоматически передаются в родительский контекст управляемого объекта. Базовые данные гарантируют, что это происходит в правильном потоке. Это зависит от основного контекста управляемого объекта, контекста родительского управляемого объекта, чтобы передать изменения в постоянный координатор хранилища.
Вывод
Параллелизм нелегко понять или реализовать, но наивно думать, что вы никогда не столкнетесь с ситуацией, когда вам нужно выполнять операции с базовыми данными в фоновом потоке.