OSX / Swift / Monitor Core Data / Schreibaktionen in Core Data nachvollziehen

Diese Seite verwendet Cookies. Durch die Nutzung unserer Seite erklären Sie sich damit einverstanden, dass wir Cookies setzen. Weitere Informationen

  • OSX / Swift / Monitor Core Data / Schreibaktionen in Core Data nachvollziehen

    Hallo,

    folgende Situation stellt sich mir. Wir haben in einer App Core Data integriert. Die App entscheidet anhand von zuvor geprüften Kriterien ob in der SQL-DB gespeichert wird oder Core Data zur Zwischenspeicherung der Daten herangezogen wird.

    Das funktioniert alles einwandfrei, bis auf, dass die gespeicherten Daten in Core Data hin und wieder nicht den berechneten Werten (tatsächlichen Werten) entsprechen. Da der Fehler sporadisch auftritt, hilft mir der Debugger nicht wirklich weiter - habe da nun schon einige Zeit investiert und bin nicht wirklich weitergekommen.

    Die berechneten Werte schreibe ich außerdem in eine Log-Datei. Es ist mir jedoch schleierhaft, warum diese nicht den gespeicherten Werten in Core Data entsprechen.

    Meine erste Vermutung war, dass hier eine Konvertierung der Daten stattfindet, dies ist jedoch nicht der Fall. ?(

    Ich habe die berechneten Werte testweise auch mal in die SQL-DB geschrieben. Hier tritt der Fehler nicht auf.

    Die Methode um Daten in Core Data zu schreiben nutzen wir in verschiedenen Apps. Noch nie ist dabei dieses Verhalten aufgetreten.

    Gibt es eine Art Monitorprogramm, welches sich - quasi als Interface - zwischen App und Core Data einklinken kann und so die zu schreiben Daten aktuell anzeigt bzw. die geschriebenen Daten mit den zu schreiben Daten abgleichen kann?

    Vielleicht gibt es ja noch andere Ansätze um dem Fehlverhalten auf die Spur zu kommen?


    Danke mal vorab.
  • MCDan schrieb:

    Evtl. fehlt irgendwo ein save: beim Managed Object Context.

    Werden die Werte evtl. in einem Background Thread berechnet und die Änderungen in CoreData nicht mit dem Main Thread/Context synchronisiert?
    Die Daten werden in der Tat in einem Background Thread berechnet und gespeichert.

    Zu einem späteren Zeitpunkt werden diese dann erneut aus Core Data gelesen. Die ausgelesen Daten stimmen mit den gespeicherten (gilt auch für die falschen Werte) überein.

    Wenn ich nun die in der Log-Datei gespeicherten Daten zum Vergleich mit den eingelesen Daten aus Core Data heranziehe, stelle ich fest, dass der o. g. Effekt hin und wieder auftritt.

    Einfacher wäre es, wenn dieser Fehler immer auftreten würde - dies ist jedoch nicht der Fall.

    @MCDan: Ich habe die Berechnungen nun alle mal im Main-Thread durchführen lassen und diese Werte dann im Anschluss in Core Data gespeichert. Hier tritt der Effekt bis jetzt nicht auf. Du hast mich also auf die richtige Spur gebracht. Vielen Dank.

    Nur nachvollziehen bzw. nachstellen lässt sich dieser Effekt, zumindest wenn ich es beabsichtige, nicht. Hast Du noch eine Idee?
  • MCDan schrieb:

    In jedem Background Thread sollte ein eigener Managed Object Context für Core Data verwendet werden und nicht der Managed Object Context aus dem Main Thread.
    Falls es hilft: Ich habe in meinem "StorageManager"-Singleton die folgende Methode, um für jeden Thread einen separaten Managed Object Context zu bekommen - nicht auf meinem Mist gewachsen, aber ich weiss die SO-Quelle (?) gerade nicht und fand die Lösung über das threadDirectory sehr charmant:

    Quellcode

    1. - (NSManagedObjectContext *)managedObjectContext
    2. {
    3. NSThread *currentThread = [NSThread currentThread];
    4. if (currentThread == [NSThread mainThread])
    5. {
    6. return self.persistentContainer.viewContext;
    7. }
    8. else
    9. {
    10. //Return separate MOC for each new thread
    11. NSManagedObjectContext *backgroundContext = [currentThread.threadDictionary objectForKey:@"MOC_KEY"];
    12. if (backgroundContext == nil)
    13. {
    14. backgroundContext = self.persistentContainer.newBackgroundContext;
    15. [currentThread.threadDictionary setObject:backgroundContext forKey:@"MOC_KEY"];
    16. }
    17. return backgroundContext;
    18. }
    19. }
    Alles anzeigen
    Mattes
    Diese Seite bleibt aus technischen Gründen unbedruckt.
  • @MCDan: Ich habe hierfür mal das Pattern Singleton, wie von @MyMattes vorgeschlagen, etabliert. Es scheint in der Tat so zu sein, dass hier der Speichervorgang kurzzeitig angehalten wird. Weshalb auch immer??? Der Zugriff bzw. das Auslesen scheint hin und wieder genau in diesem Zeitraum von statten zu gehen. ?(

    @MyMattes: Ich bin nun etwas irritiert. Das Singleton-Pattern, so wie ich es verstanden habe, sollte doch nur einmal instanziert werden können. Liegt hier nicht ein Widerspruch vor, wenn für jeden Speichervorgang ein eigener Context angestoßen wird? Kann auch sein, dass ich da etwas missverstanden habe? Umgesetz habe ich es wie folgt s. Quellcode.

    @Wolf: Sempahoren sind der nächste Versuch. Danke für die Anregung.

    C-Quellcode

    1. ...
    2. class ResCalcData {
    3. static let shared = ResCalcData()
    4. ....... // Variablen
    5. // Unterbinden, dass mehrere Instanzen erzeugt werden können
    6. privat init() {}
    7. func saveCalcData() {
    8. // Hier erzeuge ich mein Managed Object Context
    9. ...
    10. }
    11. }
    12. ...
    Alles anzeigen
  • "Singleton" bezog sich bei mir nur darauf, dass die Klasse, welche in meinen Apps für die Datenhaltung zuständig ist, als Singleton realisiert ist. Mit Deiner Thematik hat das nix zu tun. Nur deren Methode, für jeden Thread einen separaten MOC zu liefern, könnte Dir helfen...

    Mattes
    Diese Seite bleibt aus technischen Gründen unbedruckt.
  • MCDan schrieb:

    Ein Singleton schützt Dich ja nicht davor, dass saveCalcData() in beliebig vielen Threads parallel ausgeführt werden kann.
    ...
    @MCDan: Da liegst Du vollkommen richtig. :rolleyes:

    Aufgerufen wird saveCalcData() direkt nachdem die Berechnung geendet hat, also jeweils im selben Kontext. Wie aufgerufen wird s. Quellcode.

    Quellcode

    1. ...
    2. var MyCalcClass = ResCalcData.shared
    3. ...
    4. // Save calc. data
    5. MyCalcClass.saveCalcData(aDataGroup: aTmpData)
    6. ...
  • MyMattes schrieb:

    "Singleton" bezog sich bei mir nur darauf, dass die Klasse, welche in meinen Apps für die Datenhaltung zuständig ist, als Singleton realisiert ist. Mit Deiner Thematik hat das nix zu tun. Nur deren Methode, für jeden Thread einen separaten MOC zu liefern, könnte Dir helfen...

    Mattes
    Okay. Kannst Du mir kurz erläutern, weshalb mir ein jeweils separater MOC an dieser Stelle weiterhilft? Es scheint so als fehlen mir da einige Infos. Vielen Dank mal vorab, Mattes.
  • OSXDev schrieb:

    Okay. Kannst Du mir kurz erläutern, weshalb mir ein jeweils separater MOC an dieser Stelle weiterhilft? Es scheint so als fehlen mir da einige Infos. Vielen Dank mal vorab, Mattes.
    Ich habe mich bei der Nutzung von Core Data in mehreren Threads an die Vorgaben von Apple gehalten, diese nennen zwei wichtige Punkte, die sicherzustellen sind:

    Apple schrieb:


    • Managed object contexts are bound to the thread (queue) that they are associated with upon initialization.
    • Managed objects retrieved from a context are bound to the same queue that the context is bound to.
    Den ersten adressiert obiger Code, der jedem Thread einen eigenen MOC erstellt / zurückgibt. Hierzu hole ich mir den MOC immer über Aufruf der Methode managedObjectContext aus einem Singleton.

    Der zweite Punkt ergibt sich damit fast automatisch - da ein Managed Object nur im Rahmen seines MOCs bekannt ist: Die Übergabe eines Core Data Objektes an einen anderen Thread muss über die ObjectID erfolgen.

    Der Grund für beide Bedingungen ist, dass laut Apple nicht alles Core Data Objekte thread safe sind ... und meine Erfahrung hat mich gelehrt, solche Hinweise ernst zu nehmen: Ich hatte z. B. schon sehr komische Effekte, als ich Objekteigenschaften in mehreren Threads über ivars statt Properties nutzte.

    Mattes
    Diese Seite bleibt aus technischen Gründen unbedruckt.

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von MyMattes ()

  • MyMattes schrieb:

    OSXDev schrieb:

    Okay. Kannst Du mir kurz erläutern, weshalb mir ein jeweils separater MOC an dieser Stelle weiterhilft? Es scheint so als fehlen mir da einige Infos. Vielen Dank mal vorab, Mattes.
    Ich habe mich bei der Nutzung von Core Data in mehreren Threads an die Vorgaben von Apple gehalten, diese nennen zwei wichtige Punkte, die sicherzustellen sind:

    Apple schrieb:


    • Managed object contexts are bound to the thread (queue) that they are associated with upon initialization.
    • Managed objects retrieved from a context are bound to the same queue that the context is bound to.

    Den ersten adressiert obiger Code, der jedem Thread einen eigenen MOC erstellt / zurückgibt. Hierzu hole ich mir den MOC immer über Aufruf der Methode managedObjectContext aus einem Singleton.
    Der zweite Punkt ergibt sich damit fast automatisch - da ein Managed Object nur im Rahmen seines MOCs bekannt ist: Die Übergabe eines Core Data Objektes an einen anderen Thread muss über die ObjectID erfolgen.

    Der Grund für beide Bedingungen ist, dass laut Apple nicht alles Core Data Objekte thread safe sind ... und meine Erfahrung hat mich gelehrt, solche Hinweise ernst zu nehmen: Ich hatte z. B. schon sehr komische Effekte, als ich Objekteigenschaften in mehreren Threads über ivars statt Properties nutzte.

    Mattes

    Dies würde bedeuten, dass der Fehler evtl. darin zu finden ist, dass nicht jeder Thread eine eine MOC inne hat. ;(

    Ich bin nun grübeln. Meine Klasse ResCalcData ist als Singlton realisiert. Meine Methode saveCalcData() erzeugt ..... ?(

    Also - meine Singleton ResCalcData Klasse muss eine separate Methode enthalten, welche einen MOC (Beispiel - Sourcecode s. o. MyMattes) erzeugt. Hoffe ich habe dies so richtig verstanden?

    Durch den Aufruf bzw. der Generierung eines MOC kann man auf das NSMangedObject zugreifen und zur Speicherung übergebe ich meiner saveCalcData() nicht die Daten in Form eines Arrays sondern die ObjectID. Stimmt dies so?

    Nachtrag:
    Die Fehlerquelle ist im Core Data-Stack zu finden. Uff, so viel Zeit für so einen ...... . Also der Core Data-Stack beinhaltet nicht den Persistent Container. Dieser ist bedauerlicherweise kein direkter Bestandteil des Core Data-Stacks und führt somit die Verwaltung in eigenen Threads durch. Je nachdem wie fix die Zugriffe stattfinden, kann es hier zu Verzögerungen beim Schreiben bzw. Lesen kommen.

    Warum ich die Daten überhaupt auslesen kann, obwohl der Schreibvorgang nicht vollständig geendet hat, ist mir nach wie vor ein Rätsel. Muss mir nun überlegen wie ich dies in den Griff bekommen kann.

    Ein Dankeschön allen die sich hier tatkräftig eingebracht haben.

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von OSXDev () aus folgendem Grund: Nachtrag: Fehlerquelle gefunden

  • OSXDev schrieb:

    Ich bin nun grübeln. Meine Klasse ResCalcData ist als Singlton realisiert. Meine Methode saveCalcData() erzeugt ..... ?(

    Also - meine Singleton ResCalcData Klasse muss eine separate Methode enthalten, welche einen MOC (Beispiel - Sourcecode s. o. MyMattes) erzeugt. Hoffe ich habe dies so richtig verstanden?

    Durch den Aufruf bzw. der Generierung eines MOC kann man auf das NSMangedObject zugreifen und zur Speicherung übergebe ich meiner saveCalcData() nicht die Daten in Form eines Arrays sondern die ObjectID. Stimmt dies so?
    Jetzt bin ich langsam verwirrt. Ich versuche einmal, zu sortieren:
    • Du hast Berechnungen in verschiedenen Threads, diese speichern die Daten per save:. Korrekt?
    • Die Berechnung / Speicherung ist in einem Singleton realisiert, dies sagt aber nix über die Verwendung getrennter MOCs je Thread aus. Dies ist - siehe obige Apple Doc - aber dringen angeraten. Das könntest Du mit der zitierten Methode (Property-Getter) sicherstellen.
    • Du musst beim Sichern der Kontexte ebenfalls nach Threads unterscheiden - bei Verwendung des o.g. Property geschieht dies automatisch, z. B. [self.managedObjectContext save:&error].
    • Die ObjectID kommt in's Spiel, wenn Du irgendwo ein Core-Data-Object von einem Thread an einen anderen übergeben musst: Dann kannst Du eben nicht das NSManagedObject verwenden, sondern übergibst dessen objectID und der empfangene Thread ermittelt daraus wieder ein NSManagedObject in seinem MOC. Ich bin nicht sicher, ob es diesen Fall bei Dir gibt, da Deine ResCalcData-Klasse scheinbar keine CD-Objekte übergeben bekommt.
    Ich hoffe, nun nicht noch mehr verwirrt zu haben :)

    Mattes
    Diese Seite bleibt aus technischen Gründen unbedruckt.
  • MyMattes schrieb:

    OSXDev schrieb:

    Ich bin nun grübeln. Meine Klasse ResCalcData ist als Singlton realisiert. Meine Methode saveCalcData() erzeugt ..... ?(

    Also - meine Singleton ResCalcData Klasse muss eine separate Methode enthalten, welche einen MOC (Beispiel - Sourcecode s. o. MyMattes) erzeugt. Hoffe ich habe dies so richtig verstanden?

    Durch den Aufruf bzw. der Generierung eines MOC kann man auf das NSMangedObject zugreifen und zur Speicherung übergebe ich meiner saveCalcData() nicht die Daten in Form eines Arrays sondern die ObjectID. Stimmt dies so?
    Jetzt bin ich langsam verwirrt. Ich versuche einmal, zu sortieren:
    • Du hast Berechnungen in verschiedenen Threads, diese speichern die Daten per save:. Korrekt?
    • Die Berechnung / Speicherung ist in einem Singleton realisiert, dies sagt aber nix über die Verwendung getrennter MOCs je Thread aus. Dies ist - siehe obige Apple Doc - aber dringen angeraten. Das könntest Du mit der zitierten Methode (Property-Getter) sicherstellen.
    • Du musst beim Sichern der Kontexte ebenfalls nach Threads unterscheiden - bei Verwendung des o.g. Property geschieht dies automatisch, z. B. [self.managedObjectContext save:&error].
    • Die ObjectID kommt in's Spiel, wenn Du irgendwo ein Core-Data-Object von einem Thread an einen anderen übergeben musst: Dann kannst Du eben nicht das NSManagedObject verwenden, sondern übergibst dessen objectID und der empfangene Thread ermittelt daraus wieder ein NSManagedObject in seinem MOC. Ich bin nicht sicher, ob es diesen Fall bei Dir gibt, da Deine ResCalcData-Klasse scheinbar keine CD-Objekte übergeben bekommt.
    Ich hoffe, nun nicht noch mehr verwirrt zu haben :)

    Mattes
    • Ja.
    • Stimmt. Dies habe ich so auch realisiert - war nur in diesem Augenblick maximal verwirrt. Habe Deine Hinweise glaube ich durcheinander gebracht. :rolleyes: Café hat geholfen.
    • Wird so berücksichtigt. :thumbsup:
    • Auch hier liegst Du richtig.


    Das alles funktioniert so lange einwandfrei, so lange man die Leistung des Mac's berücksichtigt auf dem die App läuft. Auf einem iMacPro läuft alles reibungslos. Parallelität ist eine super Sache, solange man die Grenzen des Rechners berücksichtigt. Werde nun eine Methode hinzufügen, welche das Modell feststellt und in diesem Zuge dann festlegt wieviele Berechnungen parallel aufgerufen werden dürfen. :saint:

    Vielen Dank Mattes

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von OSXDev ()

  • OSXDev schrieb:

    Werde nun eine Methode hinzufügen, welche das Modell feststellt und in diesem Zuge dann festlegt wieviele Berechnungen parallel aufgerufen werden dürfen. :saint:
    Ich habe keine gute Erklärung für den Effekt, sage Dir aber bestimmt nichts Neues, wenn ich dieser Lösung nicht traue:

    Es klingt - wie Du ja schriebst - nach einem Timing-Problem. Ein solches über Verzögerungen statt einer sauberen Serialisierung zu lösen, ist die Anleitung zum Unglücklichsein: Irgendwann beisst Dich das in den Hintern ... beim nächsten Modell ... bei der nächsten Optimierung ... aber sicher dann, wenn Du es am wenigsten brauchen kannst. Murphy eben.

    Mein Rat: Gerne jetzt einen "Hot fix", um die Kuh vom Eis zu haben. Nimm' Dir dann aber bald die Zeit für eine abschliessende Root-Cause-Analyse. Alles andere wirst Du bereuen.

    So, das war mein Wort zum Dienstag, Mattes :)
    Diese Seite bleibt aus technischen Gründen unbedruckt.
  • OSXDev schrieb:

    Werde nun eine Methode hinzufügen, welche das Modell feststellt und in diesem Zuge dann festlegt wieviele Berechnungen parallel aufgerufen werden dürfen. :saint:
    Du hast dir die Antwort doch oben gegeben, Data Races. Und hierfür ist das keine geeignete Methode. Du musst also einen Weg gehen, damit deine Threads save werden. Dafür gibt es verschiedene Möglichkeiten… nutze sie
  • OSXDev schrieb:

    Das alles funktioniert so lange einwandfrei, so lange man die Leistung des Mac's berücksichtigt auf dem die App läuft. Auf einem iMacPro läuft alles reibungslos. Parallelität ist eine super Sache, solange man die Grenzen des Rechners berücksichtigt. Werde nun eine Methode hinzufügen, welche das Modell feststellt und in diesem Zuge dann festlegt wieviele Berechnungen parallel aufgerufen werden dürfen. :saint:
    Bitte nicht!

    Dafür gibt es u.a. Grand Central Dispatch oder Concurrency für Swift.
  • MyMattes schrieb:

    OSXDev schrieb:

    Werde nun eine Methode hinzufügen, welche das Modell feststellt und in diesem Zuge dann festlegt wieviele Berechnungen parallel aufgerufen werden dürfen. :saint:
    Ich habe keine gute Erklärung für den Effekt, sage Dir aber bestimmt nichts Neues, wenn ich dieser Lösung nicht traue:
    Es klingt - wie Du ja schriebst - nach einem Timing-Problem. Ein solches über Verzögerungen statt einer sauberen Serialisierung zu lösen, ist die Anleitung zum Unglücklichsein: Irgendwann beisst Dich das in den Hintern ... beim nächsten Modell ... bei der nächsten Optimierung ... aber sicher dann, wenn Du es am wenigsten brauchen kannst. Murphy eben.

    Mein Rat: Gerne jetzt einen "Hot fix", um die Kuh vom Eis zu haben. Nimm' Dir dann aber bald die Zeit für eine abschliessende Root-Cause-Analyse. Alles andere wirst Du bereuen.

    So, das war mein Wort zum Dienstag, Mattes :)
    Mattes, ich sitze hier schon etwas länger an der Lösung und die Zeit sitzt mir im Nacken.

    Du liegst bestimmt nicht verkehrt mit Deiner Annahme.

    Ich werde mir die Konzepte, welcher hinter den Beispielen - die MCDan gepostet hat - mal separat zu Gemüte führen.

    Nach dem ersten Durchlesen, habe ich nun nicht wirklich einen logischen Fehler in meiner Realisierung feststellen können. Aber der Teufel versteckt sich bekanntlich im Detail.

    Ich lasse dieses Thema mal noch geöffnet. Da wird bestimmt noch die ein oder andere Fragen aufkommen.

    Vielen Dank, Mattes.