14

簡単に言えば、関連するばかげた同時実行ルール(というか、同時実行のサポートが完全に欠如していて、スレッド間NSManagedObjectContextで共有しようとすると爆発したり、他の間違ったことをしたりする傾向があること)にうんざりしており、実装しようとしています。NSManagedObjectContextスレッドセーフなバリアント。

基本的に、作成されたスレッドを追跡するサブクラスを作成し、すべてのメソッド呼び出しをそのスレッドにマップします。これを行うメカニズムは少し複雑ですが、重要なのは、次のようないくつかのヘルパー メソッドがあることです。

- (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次のようなパターンに従ってインターフェイスを実装します。

- (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];
    }
}

...そして、次のようなコードでテストしています:

- (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];
    }
}

基本的に、上記のエントリ ポイントを対象とするいくつかのスレッドを起動すると、エンティティがランダムに作成および削除されます。これはほとんど正常に機能します。

問題は、スレッドの 1 つがEXC_BAD_ACCESS呼び出し時に頻繁に を取得することobj.<field> = <value>;です。objデバッガーで印刷するとすべてがうまく見えるので、何が問題なのかはっきりしません。問題の可能性 ( Apple が NSManagedObjectContext のサブクラス化を推奨していないという事実以外) とその修正方法に関する提案はありますか?

PS私は、NSOperationQueueこの問題を「解決」するために通常使用されるGCDおよびその他の手法を認識しています。それらのどれも私が欲しいものを提供していません。私が探しているのは、NSManagedObjectContext外部同期を必要とせずにアプリケーションの状態を表示および変更するために、任意の数のスレッドで自由に、安全に、直接使用できる です。

4

3 に答える 3

8

noa が正しく指摘したように、問題は、スレッドセーフにしたにもかかわらず、インスタンス自体をスレッドセーフにするようにNSManagedObjectContextインストルメント化していなかったことです。NSManagedObjectスレッド セーフなコンテキストと非スレッド セーフなエンティティ間の相互作用が、定期的なクラッシュの原因でした。

興味のあるNSManagedObject方のために、Core Data が通常生成するメソッド (の一部) の代わりに独自のセッター メソッドを挿入して、スレッド セーフなサブクラスを作成しました。これは、次のようなコードを使用して実現されます。

//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];
}

これは、スレッドセーフなコンテキストの実装と組み合わせて、問題を解決し、私が望んでいたものを手に入れました。結果を心配することなく、必要な人に渡すことができるスレッドセーフなコンテキスト。

もちろん、これは完全な解決策ではありません。少なくとも次の制限を特定したからです。

/* 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.  
 *
 */

ただし、ほとんどの典型的な小規模から中規模のプロジェクトでは、スレッドセーフなデータ層の利点がこれらの制限を大幅に上回ると思います。

于 2012-05-16T04:20:31.880 に答える
3

提供された同時実行タイプの 1 つを使用してコンテキストをインスタンス化し、performBlock / performBlockAndWait を活用してみませんか?

これは、Core Data のアクセサー メソッドの実装を台無しにする必要があるため、必要なスレッドの制限を実装します。これは、すぐにわかるように、正しく行うのが非常に苦痛であるか、ユーザーにとって非常に悪い結果になるかのいずれかです。

于 2012-05-15T05:06:42.437 に答える
1

Bart Jacobs による優れたチュートリアル「ゼロからのコア データ: iOS 5.0 以降および/または Lion 以降向けの洗練されたソリューションが必要な人のための並行性」。2 つのアプローチについて詳しく説明します。より洗練されたソリューションには、親/子の管理対象オブジェクト コンテキストが含まれます。

于 2015-08-10T21:27:16.777 に答える