使核心数据线程安全 [英] Making Core Data Thread-safe

查看:129
本文介绍了使核心数据线程安全的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

很长的故事,我厌倦了与 NSManagedObjectContext 相关的荒谬的并发规则(或者说,它完全缺乏对并发和支持爆炸或做其他不正确的事情,如果你尝试共享 NSManagedObjectContext 跨线程),并且我试图实现线程安全的变体。

Long story short, I'm tired of the absurd concurrency rules associated with NSManagedObjectContext (or rather, its complete lack of support for concurrency and tendency to explode or do other incorrect things if you attempt to share an NSManagedObjectContext across threads), and am trying to implement a thread-safe variant.

基本上我所做的是创建一个子类,跟踪创建的线程,然后将所有方法调用映射回该线程。这样做的机制有点复杂,但它的关键是我有一些帮助方法,如:

Basically what I've done is created a subclass that tracks the thread that it was created on, and then maps all method invocations back to that thread. The mechanism for doing this is slightly convoluted, but the crux of it is that I've got some helper methods like:

- (NSInvocation*) invocationWithSelector:(SEL)selector {
    //creates an NSInvocation for the given selector
    NSMethodSignature* sig = [self methodSignatureForSelector:selector];    
    NSInvocation* call = [NSInvocation invocationWithMethodSignature:sig];
    [call retainArguments];
    call.target = self;

    call.selector = selector;

    return call;
}

- (void) runInvocationOnContextThread:(NSInvocation*)invocation {
    //performs an NSInvocation on the thread associated with this context
    NSThread* currentThread = [NSThread currentThread];
    if (currentThread != myThread) {
        //call over to the correct thread
        [self performSelector:@selector(runInvocationOnContextThread:) onThread:myThread withObject:invocation waitUntilDone:YES];
    }
    else {
        //we're okay to invoke the target now
        [invocation invoke];
    }
}


- (id) runInvocationReturningObject:(NSInvocation*) call {
    //returns object types only
    [self runInvocationOnContextThread:call];

    //now grab the return value
    __unsafe_unretained id result = nil;
    [call getReturnValue:&result];
    return result;
}

...然后子类实现 NSManagedContext 接口遵循以下模式:

...and then the subclass implements the NSManagedContext interface following a pattern like:

- (NSArray*) executeFetchRequest:(NSFetchRequest *)request error:(NSError *__autoreleasing *)error {
    //if we're on the context thread, we can directly call the superclass
    if ([NSThread currentThread] == myThread) {
        return [super executeFetchRequest:request error:error];
    }

    //if we get here, we need to remap the invocation back to the context thread
    @synchronized(self) {
        //execute the call on the correct thread for this context
        NSInvocation* call = [self invocationWithSelector:@selector(executeFetchRequest:error:) andArg:request];
        [call setArgument:&error atIndex:3];
        return [self runInvocationReturningObject:call];
    }
}

...然后我测试它一些代码如下:

...and then I'm testing it with some code that goes like:

- (void) testContext:(NSManagedObjectContext*) context {
    while (true) {
        if (arc4random() % 2 == 0) {
            //insert
            MyEntity* obj = [NSEntityDescription insertNewObjectForEntityForName:@"MyEntity" inManagedObjectContext:context];
            obj.someNumber = [NSNumber numberWithDouble:1.0];
            obj.anotherNumber = [NSNumber numberWithDouble:1.0];
            obj.aString = [NSString stringWithFormat:@"%d", arc4random()];

            [context refreshObject:obj mergeChanges:YES];
            [context save:nil];
        }
        else {
            //delete
            NSArray* others = [context fetchObjectsForEntityName:@"MyEntity"];
            if ([others lastObject]) {
                MyEntity* target = [others lastObject];
                [context deleteObject:target];
                [context save:nil];
            }
        }
        [NSThread sleepForTimeInterval:0.1];
    }
}

入口点,并且它们随机创建和删除实体。

So essentially, I spin up some threads targeting the above entry point, and they randomly create and delete entities. This almost works the way it should.

问题是每一个线程都会得到一个 EXC_BAD_ACCESS 当调用 obj时。< field> =< value> ;; 。我不清楚是什么问题,因为如果我在调试器中打印 obj 一切看起来不错。关于什么问题可能是什么建议(除了苹果建议反对子类化NSManagedObjectContext的事实,以及如何解决它?)

The problem is that every so often one of the threads will get an EXC_BAD_ACCESS when calling obj.<field> = <value>;. It's not clear to me what the problem is, because if I print obj in the debugger everything looks good. Any suggestions on what the problem might be (other than the fact that Apple recommends against subclassing NSManagedObjectContext) and how to fix it?

我知道GCD和 NSOperationQueue 和其他技术通常用于解决这个问题。没有一个提供我想要的。我正在寻找的是一个 NSManagedObjectContext ,可以自由,安全,并由任何数量的线程直接使用查看和更改应用程序状态,而不需要任何外部同步。 / p>

P.S. I'm aware of GCD and NSOperationQueue and other techniques typically used to "solve" this problem. None of those offer what I want. What I'm looking for is an NSManagedObjectContext that can be freely, safely, and directly used by any number of threads to view and change application state without requiring any external synchronization.

推荐答案

由于noa正确地指出,问题是,虽然我已经使 NSManagedObjectContext 线程安全,我没有把 NSManagedObject 实例本身设置为线程安全。线程安全上下文和非线程安全实体之间的交互负责我的周期性崩溃。

As noa rightly pointed out, the problem was that although I had made the NSManagedObjectContext thread-safe, I had not instrumented the NSManagedObject instances themselves to be thread-safe. Interactions between the thread-safe context and the non-thread-safe entities were responsible for my periodic crashes.

如果有人感兴趣,我通过注入自己的setter方法创建了一个线程安全的 NSManagedObject (一些)核心数据通常会生成的。这是使用下面的代码:

In case anyone is interested, I created a thread-safe NSManagedObject subclass by injecting my own setter methods in lieu of (some of) the ones that Core Data would normally generate. This is accomplished using code like:

//implement these so that we know what thread our associated context is on
- (void) awakeFromInsert {
    myThread = [NSThread currentThread];
}
- (void) awakeFromFetch {
    myThread = [NSThread currentThread];
}

//helper for re-invoking the dynamic setter method, because the NSInvocation requires a @selector and dynamicSetter() isn't one
- (void) recallDynamicSetter:(SEL)sel withObject:(id)obj {
    dynamicSetter(self, sel, obj);
}

//mapping invocations back to the context thread
- (void) runInvocationOnCorrectThread:(NSInvocation*)call {
    if (! [self myThread] || [NSThread currentThread] == [self myThread]) {
        //okay to invoke
        [call invoke];
    }
    else {
        //remap to the correct thread
        [self performSelector:@selector(runInvocationOnCorrectThread:) onThread:myThread withObject:call waitUntilDone:YES];
    }
}

//magic!  perform the same operations that the Core Data generated setter would, but only after ensuring we are on the correct thread
void dynamicSetter(id self, SEL _cmd, id obj) {
    if (! [self myThread] || [NSThread currentThread] == [self myThread]) {
        //okay to execute
        //XXX:  clunky way to get the property name, but meh...
        NSString* targetSel = NSStringFromSelector(_cmd);
        NSString* propertyNameUpper = [targetSel substringFromIndex:3];  //remove the 'set'
        NSString* firstLetter = [[propertyNameUpper substringToIndex:1] lowercaseString];
        NSString* propertyName = [NSString stringWithFormat:@"%@%@", firstLetter, [propertyNameUpper substringFromIndex:1]];
        propertyName = [propertyName substringToIndex:[propertyName length] - 1];

        //NSLog(@"Setting property:  name=%@", propertyName);

        [self willChangeValueForKey:propertyName];
        [self setPrimitiveValue:obj forKey:propertyName];
        [self didChangeValueForKey:propertyName];

    }
    else {
        //call back on the correct thread
        NSMethodSignature* sig = [self methodSignatureForSelector:@selector(recallDynamicSetter:withObject:)];
        NSInvocation* call = [NSInvocation invocationWithMethodSignature:sig];
        [call retainArguments];
        call.target = self;
        call.selector = @selector(recallDynamicSetter:withObject:);
        [call setArgument:&_cmd atIndex:2];
        [call setArgument:&obj atIndex:3];

        [self runInvocationOnCorrectThread:call];
    }
}

//bootstrapping the magic; watch for setters and override each one we see
+ (BOOL) resolveInstanceMethod:(SEL)sel {
    NSString* targetSel = NSStringFromSelector(sel);
    if ([targetSel startsWith:@"set"] && ! [targetSel contains:@"Primitive"]) {
        NSLog(@"Overriding selector:  %@", targetSel);
        class_addMethod([self class], sel, (IMP)dynamicSetter, "v@:@");
        return YES;
    }

    return [super resolveInstanceMethod:sel];
}

这与我的线程安全上下文实现结合,解决了问题,让我想要什么;一个线程安全的上下文,我可以传递给任何我想要的,而不必担心后果。

This, in conjunction with my thread-safe context implementation, solved the problem and got me what I wanted; a thread-safe context that I can pass around to whomever I want without having to worry about the consequences.

当然,这不是一个防弹解决方案,因为我已经确定了至少以下限制:

Of course this is not a bulletproof solution, as I have identified at least the following limitations:

/* Also note that using this tool carries several small caveats:
 *
 *      1.  All entities in the data model MUST inherit from 'ThreadSafeManagedObject'.  Inheriting directly from 
 *          NSManagedObject is not acceptable and WILL crash the app.  Either every entity is thread-safe, or none 
 *          of them are.
 *
 *      2.  You MUST use 'ThreadSafeContext' instead of 'NSManagedObjectContext'.  If you don't do this then there 
 *          is no point in using 'ThreadSafeManagedObject' (and vice-versa).  You need to use the two classes together, 
 *          or not at all.  Note that to "use" ThreadSafeContext, all you have to do is replace every [[NSManagedObjectContext alloc] init]
 *          with an [[ThreadSafeContext alloc] init].
 *
 *      3.  You SHOULD NOT give any 'ThreadSafeManagedObject' a custom setter implementation.  If you implement a custom 
 *          setter, then ThreadSafeManagedObject will not be able to synchronize it, and the data model will no longer 
 *          be thread-safe.  Note that it is technically possible to work around this, by replicating the synchronization
 *          logic on a one-off basis for each custom setter added.
 *
 *      4.  You SHOULD NOT add any additional @dynamic properties to your object, or any additional custom methods named
 *          like 'set...'.  If you do the 'ThreadSafeManagedObject' superclass may attempt to override and synchronize 
 *          your implementation.
 *
 *      5.  If you implement 'awakeFromInsert' or 'awakeFromFetch' in your data model class(es), thne you MUST call 
 *          the superclass implementation of these methods before you do anything else.
 *
 *      6.  You SHOULD NOT directly invoke 'setPrimitiveValue:forKey:' or any variant thereof.  
 *
 */

但是,对于大多数典型的中小型项目我认为线程安全数据层的好处显着超过这些限制。

However, for most typical small to medium-sized projects I think the benefits of a thread-safe data layer significantly outweigh these limitations.

这篇关于使核心数据线程安全的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆