Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Core Data

Core Data von Grund auf neu: Parallelität

by
Read Time:11 minsLanguages:
This post is part of a series called Core Data from Scratch.
Core Data from Scratch: Subclassing NSManagedObject
iOS 8: Core Data and Batch Updates

German (Deutsch) translation by Alex Grigorovich (you can also view the original English article)

Wenn Sie eine kleine oder einfache Anwendung entwickeln, sehen Sie den Vorteil der Ausführung von Core Data-Vorgängen wahrscheinlich nicht im Hintergrund. Was würde jedoch passieren, wenn Sie beim ersten Start Ihrer Anwendung Hunderte oder Tausende von Datensätzen in den Hauptthread importieren würden? Die Folgen könnten dramatisch sein. Beispielsweise könnte Ihre Anwendung von Apples Watchdog beendet werden, da der Start zu lange dauert.

In diesem Artikel werfen wir einen Blick auf die Gefahren bei der Verwendung von Core Data in mehreren Threads und untersuchen verschiedene Lösungen, um das Problem anzugehen.

1. Thread-Sicherheit

Wenn Sie mit Core Data arbeiten, ist es wichtig, immer daran zu denken, dass Core Data nicht threadsicher ist. Core Data wird voraussichtlich auf einem einzelnen Thread ausgeführt. Dies bedeutet nicht, dass jede Core Data-Operation auf dem Hauptthread ausgeführt werden muss, was für UIKit gilt, aber es bedeutet, dass Sie wissen müssen, welche Operationen auf welchen Threads ausgeführt werden. Dies bedeutet auch, dass Sie vorsichtig sein müssen, wie Änderungen von einem Thread auf andere Threads übertragen werden.

Die Arbeit mit Core Data an mehreren Threads ist aus theoretischer Sicht eigentlich sehr einfach. NSManagedObject, NSManagedObjectContext und NSPersistentStoreCoordinator sind nicht threadsicher, und auf Instanzen dieser Klassen sollte nur über den Thread zugegriffen werden, auf dem sie erstellt wurden. Wie Sie sich vorstellen können, wird dies in der Praxis etwas komplexer.

NSManagedObject

Wir wissen bereits, dass die NSManagedObject-Klasse nicht threadsicher ist, aber wie greifen Sie von verschiedenen Threads auf einen Datensatz zu? Eine NSManagedObject-Instanz verfügt über eine objectID-Methode, die eine Instanz der NSManagedObjectID-Klasse zurückgibt. Die NSManagedObjectID-Klasse ist threadsicher und eine Instanz enthält alle Informationen, die ein verwalteter Objektkontext zum Abrufen des entsprechenden verwalteten Objekts benötigt.

Im folgenden Codeausschnitt fragen wir einen verwalteten Objektkontext nach dem verwalteten Objekt, das der objektID entspricht. Die Methoden objectWithID: und existentObjectWithID:error: geben eine lokale Version des entsprechenden verwalteten Objekts zurück, die für den aktuellen Thread lokal ist.

Die grundlegende Regel, an die Sie sich erinnern sollten, besteht darin, die NSManagedObject-Instanz nicht von einem Thread an einen anderen zu übergeben. Übergeben Sie stattdessen die objektID des verwalteten Objekts und fragen Sie den Kontext des verwalteten Objekts des Threads nach einer lokalen Version des verwalteten Objekts.

NSManagedObjectContext

Da die NSManagedObjectContext-Klasse nicht threadsicher ist, können wir für jeden Thread, der mit Core Data interagiert, einen verwalteten Objektkontext erstellen. Diese Strategie wird häufig als Thread-Beschränkung bezeichnet.

Ein gängiger Ansatz besteht darin, den Kontext des verwalteten Objekts im Wörterbuch des Threads zu speichern, einem Wörterbuch zum Speichern von threadspezifischen Daten. Schauen Sie sich das folgende Beispiel an, um zu sehen, wie dies in der Praxis funktioniert.

NSPersistentStoreCoordinator

Was ist mit dem beständigen Geschäftskoordinator? Müssen Sie für jeden Thread einen separaten permanenten Speicherkoordinator erstellen? Dies ist zwar möglich und eine der von Apple empfohlenen Strategien, aber nicht erforderlich.

Die NSPersistentStoreCoordinator-Klasse wurde entwickelt, um mehrere verwaltete Objektkontexte zu unterstützen, selbst wenn diese verwalteten Objektkontexte in verschiedenen Threads erstellt wurden. Da die NSManagedObjectContext-Klasse den persistenten Speicherkoordinator beim Zugriff sperrt, können mehrere verwaltete Objektkontexte denselben persistenten Speicherkoordinator verwenden, selbst wenn diese verwalteten Objektkontexte in verschiedenen Threads ausgeführt werden. Dies macht ein Multithread-Core-Data-Setup viel einfacher zu verwalten und weniger komplex.

2. Parallelitätsstrategien

Bisher haben wir erfahren, dass Sie mehrere verwaltete Objektkontexte benötigen, wenn Sie Core Data-Vorgänge für mehrere Threads ausführen. Die Einschränkung besteht jedoch darin, dass verwaltete Objektkontexte sich der Existenz des anderen nicht bewusst sind. Änderungen an einem verwalteten Objekt in einem verwalteten Objektkontext werden nicht automatisch auf andere verwaltete Objektkontexte übertragen. Wie lösen wir dieses Problem?

Apple empfiehlt zwei beliebte Strategien: Benachrichtigungen und von Eltern und Kindern verwaltete Objektkontexte. Schauen wir uns jede Strategie an und untersuchen ihre Vor- und Nachteile.

Das Szenario, das wir als Beispiel nehmen, ist eine NSOperation-Unterklasse, die Arbeiten im Hintergrund ausführt und auf Kerndaten im Hintergrundthread der Operation zugreift. Dieses Beispiel zeigt Ihnen die Unterschiede und Vorteile jeder Strategie.

Strategie 1: Benachrichtigungen

Zu Beginn dieser Serie habe ich Sie in die NSFetchedResultsController-Klasse eingeführt und festgestellt, dass in einem verwalteten Objektkontext drei Arten von Benachrichtigungen veröffentlicht werden:

  • NSManagedObjectContextObjectsDidChangeNotification: Diese Benachrichtigung wird veröffentlicht, wenn sich eines der verwalteten Objekte des verwalteten Objektkontexts geändert hat
  • NSManagedObjectContextWillSaveNotification: Diese Benachrichtigung wird veröffentlicht, bevor der Kontext des verwalteten Objekts einen Speichervorgang ausführt
  • NSManagedObjectContextDidSaveNotification: Diese Benachrichtigung wird veröffentlicht, nachdem der Kontext des verwalteten Objekts einen Speichervorgang ausgeführt hat

Wenn ein verwalteter Objektkontext seine Änderungen über den Koordinator für persistente Speicher in einem persistenten Speicher speichert, möchten andere verwaltete Objektkontexte möglicherweise Informationen zu diesen Änderungen erhalten. Dies ist sehr einfach und es ist noch einfacher, die Änderungen in einen anderen verwalteten Objektkontext aufzunehmen oder zusammenzuführen. Reden wir über Code.

Wir erstellen eine nicht gleichzeitige Operation, die einige Arbeiten im Hintergrund ausführt und Zugriff auf Core Data benötigt. Der Header würde ähnlich dem unten gezeigten aussehen.

Die Benutzeroberfläche der Operation ist sehr einfach, da sie nur eine Eigenschaft für den Hauptkontext des verwalteten Objekts der Anwendung enthält. Es gibt mehrere Gründe, einen Verweis auf den Hauptkontext des verwalteten Objekts der Anwendung beizubehalten. Dies wird deutlich, wenn wir die Implementierung der TSPImportOperation-Klasse untersuchen.

Wir deklarieren zuerst eine private Eigenschaft, privateManagedObjectContext, vom Typ NSManagedObjectContext. Dies ist der verwaltete Objektkontext, den der Vorgang intern zum Ausführen von Core Data-Aufgaben verwendet.

Da wir eine nicht gleichzeitige NSOperation-Unterklasse implementieren, müssen wir die main-Methode implementieren. So sieht es aus.

Es gibt einige wichtige Details, die geklärt werden müssen. Wir initialisieren den Kontext des privaten verwalteten Objekts und legen seine Eigenschaft für den persistenten Speicherkoordinator mithilfe des mainManagedObjectContext-Objekts fest. Dies ist vollkommen in Ordnung, da wir nicht auf den mainManagedObjectContext zugreifen, sondern ihn nur um seinen Verweis auf den persistenten Speicherkoordinator der Anwendung bitten. Wir verstoßen nicht gegen die Thread-Beschränkungsregel.

Es ist wichtig, den Kontext des privaten verwalteten Objekts in der main-Methode der Operation zu initialisieren, da diese Methode auf dem Hintergrundthread ausgeführt wird, auf dem die Operation ausgeführt wird. Können wir den Kontext des verwalteten Objekts nicht in der init-Methode der Operation initialisieren? Die Antwort ist nein. Die init-Methode der Operation wird auf dem Thread ausgeführt, auf dem die TSPImportOperation initialisiert wird. Dies ist höchstwahrscheinlich der Hauptthread. Dies würde den Zweck eines privaten verwalteten Objektkontexts zunichte machen.

In der Hauptmethode der Operation fügen wir die TSPImportOperation-Instanz als Beobachter aller NSManagedObjectContextDidSaveNotification-Benachrichtigungen hinzu, die vom Kontext des privaten verwalteten Objekts veröffentlicht werden.

Anschließend erledigen wir die Arbeit, für die die Operation erstellt wurde, und speichern die Änderungen des privaten Kontexts für verwaltete Objekte, wodurch eine Benachrichtigung über NSManagedObjectContextDidSaveNotification ausgelöst wird. Lassen Sie uns einen Blick darauf werfen, was in der MethodeagedObjectContextDidSave: passiert.

Wie Sie sehen können, ist die Implementierung kurz und einfach. Wir rufen mergeChangesFromContextDidSaveNotification auf: Übergeben Sie im Hauptkontext des verwalteten Objekts das Benachrichtigungsobjekt. Wie bereits erwähnt, enthält die Benachrichtigung die Änderungen, Einfügungen, Aktualisierungen und Löschungen des Kontexts für privat verwaltete Objekte. Es ist wichtig, diese Methode für den Thread aufzurufen, für den der Hauptkontext für verwaltete Objekte erstellt wurde, den Hauptthread. Deshalb senden wir diesen Aufruf an den Haupt-Thread.

Das Verwenden der TSPImportOperation-Klasse ist so einfach wie das Initialisieren einer Instanz, das Festlegen ihrer mainManagedObjectContext-Eigenschaft und das Hinzufügen der Operation zu einer Operationswarteschlange.

Strategie 2: Übergeordnete/untergeordnete verwaltete Objektkontexte

Seit iOS 6 gibt es eine noch bessere und elegantere Strategie. Lassen Sie uns die TSPImportOperation-Klasse erneut besuchen und übergeordnete/untergeordnete verwaltete Objektkontexte nutzen. Das Konzept hinter übergeordneten/untergeordneten verwalteten Objektkontexten ist einfach, aber leistungsstark. Lassen Sie mich erklären, wie es funktioniert.

Ein untergeordneter verwalteter Objektkontext ist abhängig von seinem übergeordneten verwalteten Objektkontext, um seine Änderungen im entsprechenden persistenten Speicher zu speichern. Tatsächlich hat ein untergeordneter verwalteter Objektkontext keinen Zugriff auf einen persistenten Geschäftskoordinator. Immer wenn ein untergeordneter verwalteter Objektkontext gespeichert wird, werden die darin enthaltenen Änderungen in den übergeordneten verwalteten Objektkontext übertragen. Es ist nicht erforderlich, Benachrichtigungen zu verwenden, um die Änderungen manuell in den Haupt- oder übergeordneten verwalteten Objektkontext einzufügen.

Ein weiterer Vorteil ist die Leistung. Da der untergeordnete verwaltete Objektkontext keinen Zugriff auf den persistenten Speicherkoordinator hat, werden die Änderungen nicht auf diesen übertragen, wenn der untergeordnete verwaltete Objektkontext gespeichert wird. Stattdessen werden die Änderungen in den übergeordneten Kontext des verwalteten Objekts übertragen, wodurch dieser verschmutzt wird. Die Änderungen werden nicht automatisch an den persistenten Speicherkoordinator weitergegeben.

Verwaltete Objektkontexte können verschachtelt werden. Ein untergeordneter verwalteter Objektkontext kann einen eigenen untergeordneten verwalteten Objektkontext haben. Es gelten die gleichen Regeln. Es ist jedoch wichtig zu beachten, dass die Änderungen, die in den übergeordneten verwalteten Objektkontext übertragen werden, nicht in andere untergeordnete verwaltete Objektkontexte übertragen werden. Wenn Kind A seine Änderungen an sein Elternteil weitergegeben hat, sind Kind B diese Änderungen nicht bekannt.

Das Erstellen eines untergeordneten verwalteten Objektkontexts unterscheidet sich geringfügig von dem, was wir bisher gesehen haben. Ein untergeordneter verwalteter Objektkontext verwendet einen anderen Initialisierer, initWithConcurrencyType:. Der vom Initialisierer akzeptierte Parallelitätstyp definiert das Threading-Modell des Kontexts des verwalteten Objekts. Schauen wir uns jeden Parallelitätstyp an.

  • NSMainQueueConcurrencyType: Auf den Kontext des verwalteten Objekts kann nur über den Hauptthread zugegriffen werden. Eine Ausnahme wird ausgelöst, wenn Sie versuchen, von einem anderen Thread aus darauf zuzugreifen.
  • NSPrivateQueueConcurrencyType: Wenn Sie einen verwalteten Objektkontext mit dem Parallelitätstyp NSPrivateQueueConcurrencyType erstellen, wird der verwaltete Objektkontext einer privaten Warteschlange zugeordnet und kann nur von dieser privaten Warteschlange aus aufgerufen werden.
  • NSConfinementConcurrencyType: Dies ist der Parallelitätstyp, der dem zuvor untersuchten Thread-Confinement-Konzept entspricht. Wenn Sie einen verwalteten Objektkontext mit der init-Methode erstellen, lautet der Parallelitätstyp dieses verwalteten Objektkontexts NSConfinementConcurrencyType.

Es gibt zwei wichtige Methoden, die dem Core Data-Framework hinzugefügt wurden, als Apple übergeordnete / untergeordnete verwaltete Objektkontexte einführte: performBlock: und performBlockAndWait:. Beide Methoden werden Ihr Leben viel einfacher machen. Wenn Sie performBlock: in einem verwalteten Objektkontext aufrufen und einen Codeblock zur Ausführung übergeben, stellt Core Data sicher, dass der Block auf dem richtigen Thread ausgeführt wird. Im Fall des Parallelitätstyps NSPrivateQueueConcurrencyType bedeutet dies, dass der Block in der privaten Warteschlange dieses verwalteten Objektkontexts ausgeführt wird.

Der Unterschied zwischen performBlock: und performBlockAndWait: ist einfach. Die performBlock:-Methode blockiert den aktuellen Thread nicht. Es akzeptiert den Block, plant die Ausführung in der richtigen Warteschlange und fährt mit der Ausführung der nächsten Anweisung fort.

Die performBlockAndWait:-Methode blockiert jedoch. Der Thread, von dem performBlockAndWait: aufgerufen wird, wartet auf den Abschluss des Blocks, der an die Methode übergeben wird, bevor die nächste Anweisung ausgeführt wird. Der Vorteil ist, dass verschachtelte Aufrufe von performBlockAndWait: der Reihe nach ausgeführt werden.

Um diesen Artikel zu beenden, möchte ich die TSPImportOperation-Klasse umgestalten, um die Vorteile von übergeordneten/untergeordneten verwalteten Objektkontexten zu nutzen. Sie werden schnell feststellen, dass dies die TSPImportOperation-Unterklasse erheblich vereinfacht.

Der Header bleibt unverändert, aber die main-Methode ändert sich ziemlich stark. Schauen Sie sich die aktualisierte Implementierung unten an.

Das ist es. Der Hauptkontext für verwaltete Objekte ist das übergeordnete Element des Kontexts für private verwaltete Objekte. Beachten Sie, dass wir die Eigenschaft persistentStoreCoordinator des Kontexts für privat verwaltete Objekte nicht festlegen und die Operation nicht als Beobachter für NSManagedObjectContextDidSaveNotification-Benachrichtigungen hinzufügen. Wenn der private verwaltete Objektkontext gespeichert wird, werden die Änderungen automatisch in den übergeordneten verwalteten Objektkontext übertragen. Core Data stellt sicher, dass dies auf dem richtigen Thread geschieht. Es liegt am Hauptkontext des verwalteten Objekts, dem übergeordneten Kontext des verwalteten Objekts, die Änderungen an den persistenten Speicherkoordinator zu übertragen.

Abschluss

Parallelität ist nicht einfach zu erfassen oder zu implementieren, aber es ist naiv zu glauben, dass Sie nie auf eine Situation stoßen werden, in der Sie Core Data-Vorgänge für einen Hintergrundthread ausführen müssen.

In den nächsten beiden Artikeln werde ich Ihnen etwas über iOS 8 und Core Data erzählen. Apple hat eine Reihe neuer APIs in iOS 8 und OS X 10.10 eingeführt, darunter Stapelaktualisierung und asynchrones Abrufen.

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.