gpt4 book ai didi

ios - 使核心数据线程安全

转载 作者:技术小花猫 更新时间:2023-10-29 10:25:56 26 4
gpt4 key购买 nike

长话短说,我厌倦了与 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];
}
}

所以基本上,我启动了一些针对上述入口点的线程,它们随机创建和删除实体。这几乎可以正常工作。

问题是每隔一段时间,其中一个线程会得到一个EXC_BAD_ACCESS。打电话时 obj.<field> = <value>; .我不清楚问题出在哪里,因为如果我打印 obj在调试器中,一切看起来都不错。关于问题可能是什么的任何建议(除了 Apple 建议不要子类化 NSManagedObjectContext)以及如何解决它?

附言我知道 GCD 和 NSOperationQueue以及通常用于“解决”此问题的其他技术。这些都不提供我想要的。我要找的是 NSManagedObjectContext任何数量的线程都可以自由、安全和直接地使用它来查看和更改应用程序状态,而无需任何外部同步。

最佳答案

正如 noa 正确指出的那样,问题是虽然我已经使 NSManagedObjectContext 线程安全,但我没有将 NSManagedObject 实例本身设置为线程安全的.线程安全上下文和非线程安全实体之间的交互是我周期性崩溃的原因。

如果有人感兴趣,我创建了一个线程安全的 NSManagedObject 子类,方法是注入(inject)我自己的 setter 方法来代替 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.
*
*/

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

关于ios - 使核心数据线程安全,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10593735/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com