4

Like many iOS developers, I use CoreData, and like many iOS developers that use CoreData, I have hard-to-track-down thread violation errors. I'm trying to implement a debugging strategy for throwing exceptions when the CoreData concurrency rules are broken. My attempt is below - my question is, is this valid? Will it produce false positives?

Summary: when an NSManagedObject is created, note the thread. Whenever a value is accessed later on, check if the current thread is the same as the creation thread, and throw an exception if not.

#import "NSManagedObject+DebugTracking.h"
#import "NSObject+DTRuntime.h"
#import <objc/runtime.h>
#import "NSManagedObjectContext+DebugThreadTracking.h"

@implementation NSManagedObject (DebugTracking)

+(void)load {

    [NSManagedObject swizzleMethod:@selector(willAccessValueForKey:) withMethod:@selector(swizzled_willAccessValueForKey:)];
    [NSManagedObject swizzleMethod:@selector(initWithEntity:insertIntoManagedObjectContext:) withMethod:@selector(swizzled_initWithEntity:insertIntoManagedObjectContext:)];

}

- (__kindof NSManagedObject *)swizzled_initWithEntity:(NSEntityDescription *)entity
              insertIntoManagedObjectContext:(NSManagedObjectContext *)context
{
    NSManagedObject *object = [self swizzled_initWithEntity:entity insertIntoManagedObjectContext:context];
    NSLog(@"Initialising an object of type: %@", NSStringFromClass([self class]));

    object.debugThread = [NSThread currentThread];
    return object;
}

-(void)swizzled_willAccessValueForKey:(NSString *)key {

    NSThread *thread = self.debugThread;

    if (!thread) {
        NSLog(@"No Thread set");
    } else if (thread != [NSThread currentThread]) {
        [NSException raise:@"CoreData thread violation exception" format:@"Property accessed from a different thread than the object's creation thread. Type: %@", NSStringFromClass([self class])];
    } else {
        NSLog(@"All is well");
    }

    [self swizzled_willAccessValueForKey: key];
}

-(NSThread *)debugThread {
    return objc_getAssociatedObject(self, @selector(debugThread));
}

-(void)setDebugThread:(NSThread *)debugThread {
    objc_setAssociatedObject(self, @selector(debugThread), debugThread, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end
rmaddy
  • 314,917
  • 42
  • 532
  • 579
Nick Locking
  • 2,147
  • 2
  • 26
  • 42

3 Answers3

13

No, that's not a good idea. You're going to a fair amount of trouble to partially replicate something that Apple builds in to the framework.

If you edit the scheme for the target, you can add -com.apple.CoreData.ConcurrencyDebug 1 to the arguments passed on launch:

Concurrency debug flag

Once you've done that, all concurrency violations will cause an immediate crash. You'll know it's a concurrency violation because of the "all that is left to us is honor" message in the stack trace:

all that is left to us is honor

You'll see the exact line of code that caused the concurrency violation.

You may want to add a couple of other Core Data debug arguments, com.apple.CoreData.SQLDebug and com.apple.CoreData.Logging.stderr as well. They won't change the concurrency debugging but they will make Xcode print a message reading CoreData: annotation: Core Data multi-threading assertions enabled so that you'll know it's on.

extra debug flags

Tom Harrington
  • 69,312
  • 10
  • 146
  • 170
  • I have found this to be very unreliable - generally it only throws the error when something tries to save the objects to a store, but will not throw an error if, say, something from the wrong queue tries to read an NSManagedObject's properties. – Nick Locking Dec 20 '16 at 22:34
  • 2
    I noticed that If you use NSAsynchronousFetchRequest this flag will still crash the app even if its used correctly. – Stefan Nestorov Dec 23 '16 at 10:15
1

No, it is not valid. Your code supposes that a context always execute blocks on the same thread. But in fact it always run on the same QUEUE, queue and thread are not the same thing.

For example, try to use performBlockAndWait: to check if it uses the same thread performBlock: runs on. FWIK performBlockAndWait: uses the thread where it is invoked.

Gabriel
  • 3,319
  • 1
  • 16
  • 21
  • I would assume `performBlockAndWait:` uses `dispatch_sync`, which, empirically, _often_ has the behaviour you mention but may not depending on your OS version, device, etc, etc. But whatever. You're completely right: the author has inappropriately conflated threads and queues. – Tommy Dec 20 '16 at 20:18
0

You can add a debug flag that displays a lot of information about the operations performed by Core Data, including, but not limited to, the underlying SQL operations. See this answer. Mind that you can change the debug level from 1 to 2 or 3 to get even more debug info.

I'm not sure whether it will help in your specific case, without knowing the exact problem and context, however, it should help. Also, you can check the stack trace, which improved a lot for multi-threading debugging in Xcode 7 and 8.

Community
  • 1
  • 1
Gianluca Tranchedone
  • 3,598
  • 1
  • 18
  • 33