【问题标题】:Core Data saving in multiple threads在多个线程中保存核心数据
【发布时间】:2014-09-10 04:37:55
【问题描述】:

我对 Core Data 多线程保存有点困惑。

我有以下NSManagedObjectContext 设置(与MagicalRecord 相同):

SavingContext (NSPrivateQueueConcurrencyType) has child DefaultContext(NSMainQueueConcurrencyType)

每个保存线程都有自己的上下文 (NSPrivateQueueConcurrencyType),DefaultContext 作为父线程。

所以问题是:如果我需要保证唯一性,我该如何依赖在不同线程上保存相同的类型?

这是一个小测试示例(Test 是 NSManagedObject 的子类):

@implementation Test

+ (instancetype) testWithValue:(NSString *) str {
    [NSThread sleepForTimeInterval:3];
    Test *t = [Test MR_findFirstByAttribute:@"uniqueField" withValue:str];

    if (!t) {
        NSLog(@"No test found!");
        t = [Test MR_createEntity];
    }

    t.uniqueField = str;

    return t;
}
@end

它首先检查新创建的线程上下文中是否有Test(它具有父级DefaultContext),如果没有,则在当前线程上下文中创建它。

这里是测试代码:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 2;

[queue addOperationWithBlock:^{
    [Test operationWithValue:@"1"];
    [[NSManagedObjectContext MR_contextForCurrentThread] MR_saveToPersistentStoreAndWait];
}];

[queue addOperationWithBlock:^{
    [Test operationWithValue:@"1"];
    [[NSManagedObjectContext MR_contextForCurrentThread] MR_saveToPersistentStoreAndWait];
}];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"Total tests: %lu", (unsigned long)[Test MR_countOfEntities]);
    [Test MR_truncateAll];
    [[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait];
});

它只运行两个操作,并尝试保存相同的数据。创建测试后,我保存所有上下文(当前线程、默认上下文和根保存上下文)。大多数时候会有2个测试。您可以修改和添加信号量以确保两个线程同时进行检查。

2014 年 9 月 20 日更新 我已经添加了 mitrenegade 提供的代码(见下文),现在我的 Test.m 有了一个功能:

-(BOOL)validateUniqueField:(id *)ioValue error:(NSError **)outError {

    // The property being validated must not already exist

    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([self class])];
    fetchRequest.predicate = [NSPredicate predicateWithFormat:@"uniqueField == %@ AND self != %@", *ioValue, self];

    int count = [self.managedObjectContext countForFetchRequest:fetchRequest error:nil];
    if (count > 0) {
        NSLog(@"Thread: %@ (isMain: %hhd), Validation failed!", [NSThread currentThread], [NSThread isMainThread]);
        return NO;
    }

    NSLog(@"Thread: %@ (isMain: %hhd), Validation succeeded!", [NSThread currentThread], [NSThread isMainThread]);
    return YES;
}

两个线程创建相同的值(测试示例在帖子的开头)我有以下输出:

2014-09-20 11:48:53.824 coreDataTest[892:289814] Thread: <NSThread: 0x15d38940>{number = 3, name = (null)} (isMain: 0), Validation succeeded!
2014-09-20 11:48:53.826 coreDataTest[892:289815] Thread: <NSThread: 0x15e434a0>{number = 2, name = (null)} (isMain: 0), Validation succeeded!
2014-09-20 11:48:53.830 coreDataTest[892:289750] Thread: <NSThread: 0x15e172c0>{number = 1, name = main} (isMain: 1), Validation failed!
2014-09-20 11:48:53.833 coreDataTest[892:289750] Thread: <NSThread: 0x15e172c0>{number = 1, name = main} (isMain: 1), Validation failed!
2014-09-20 11:48:53.837 coreDataTest[892:289750] Thread: <NSThread: 0x15e172c0>{number = 1, name = main} (isMain: 1), Validation failed!
2014-09-20 11:48:53.839 coreDataTest[892:289750] Thread: <NSThread: 0x15e172c0>{number = 1, name = main} (isMain: 1), Validation failed!
2014-09-20 11:48:56.251 coreDataTest[892:289750] Total tests: 2

但是如果我查看底层的 sqlite 文件,根本就没有记录(这意味着它们卡在主上下文中)

【问题讨论】:

  • 不要再使用 contextForCurrentThread。您的应用崩溃,而且是随机且难以追踪的。 NSOperationQueue 现在是使用 GCD 实现的。这意味着并发模型基于队列,而不是线程。可以使用单个队列(轻松地)跨越线程边界。底线,停止使用 contextForCurrentThread
  • @casademora 所以正确的方法是将NSManagedObjectContext 传递给每个模型构造函数并使用您答案中的模式?
  • @user2786037 您应该停止使用MR_contextForCurrentThread,因为它不能保证您不会在超过 1 个线程上运行代码(请参阅 casademora 答案)。这是他博客上的文章:saulmora.com/2013/09/15/…

标签: ios multithreading core-data magicalrecord


【解决方案1】:

似乎没有对您的实际对象进行任何形式的验证检查,因此“uniqueField”属性设置为“1”的两个对象并不意味着它们不能同时存在,根据您提供的型号。

当两个线程都在运行时,每个线程都会插入一个新对象,该对象具有与某个属性(“uniqueField”)关联的某个值(“1”)。当 Core Data 合并上下文时,没有规则说这是禁止的,所以主上下文中会有两个对象。它们是具有唯一 objectID 的唯一对象。如果您使用“name”=“John”创建了两个“Person”对象,也会发生同样的事情。

如果您正确格式化签名,核心数据会自动为每个字段调用某些验证方法,如此处所示。

https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/KeyValueCoding/Articles/Validation.html

在您的 NSManagedObject 子类(Test.m)中,您需要有一个带有签名的方法

-(BOOL)valide<YourFieldName>:error:

所以尝试将此代码添加到您的 Test.m 中,并在其上放置一个断点。保存上下文时应调用此方法。

-(BOOL)validateUniqueField:(id *)ioValue error:(NSError * __autoreleasing *)outError{

    // The property being validated must not already exist

    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([self class])];
    fetchRequest.predicate = [NSPredicate predicateWithFormat:@"uniqueField == %@", *ioValue];

    int count = [self.managedObjectContext countForFetchRequest:fetchRequest error:nil];
    if (count > 0) {
        if (outError != NULL) {
            NSString *errorString = NSLocalizedString(
                                                          @"Object must have unique value for property",
                                                          @"validation: nonunique property");
                NSDictionary *userInfoDict = @{ NSLocalizedDescriptionKey : errorString };
                *outError = [[NSError alloc] initWithDomain:nil
                                                       code:0
                                                   userInfo:userInfoDict];
        }
        return NO;
    }
    return YES;
}

当上下文保存时,核心数据会自动调用此验证。你可以在这里做任何你想做的事;我正在添加执行获取并比较计数的逻辑。

编辑:我在这个话题之后不久提出了这个问题,并收到了一些答案,但没有什么超级确定的。所以我想把它说出来,我的答案适用于我们目前的情况,但显然不是一个好的效率。但是,我还没有找到适用于多个线程的解决方案,而无需在 validateForInsert 中做任何事情。据我所知,没有办法只将参数设置为在数据库中是唯一的。

Is doing a fetch request in validateForInsert overly expensive

【讨论】:

  • testWithValue 方法中,我使用[Test MR_findFirstByAttribute:@"uniqueField" withValue:str]; 这实际上是NSFetchRequest 检查TestuniqueField = 1 的对象。据我了解附加链接没有验证适合我的需要,因为它们都发生在单独的上下文中。那么有没有办法拒绝第二次保存呢? (或以某种方式合并)
  • 这是插入时的检查。由于您设置了在插入时两个线程中都不应该存在任何对象的情况,因此此检查当然会成功。你必须为你的 NSManagedObject 编写一个验证方法。我会更新我的答案。
【解决方案2】:

MagicalRecord 已经实现了在后台队列上执行保存所需的大部分工作。查看+[MagicalRecord saveWithBlock](或MR 3.0 中的[MagicalRecordStack saveWithBlock])。此方法将为您将保存操作分派到后台队列中。但是,为了使其正常工作,您必须在后台上下文中进行数据更新,以免跨越线程边界。通常,使用以下模式:

Test *testObj = ///....
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){

   Test *localTest = [testObj MR_inContext:localContext];
   //localTest.property = newValue
}]; ///Exiting the block will save the context and your changes.

【讨论】:

  • 我不确定我是否正确:我应该在 Default Context 中搜索 testObj 吗?如果没有 - 那么我将在子上下文中搜索,那将不知道其他子上下文。所以这种模式不能保证唯一性,对吧?
【解决方案3】:

查看this link 以获得最佳并发设置。

我发现在多个线程之间协调保存的最佳方式是通过通知 (NSManagedObjectContextDidSaveNotification)。当一个上下文保存时,系统会向其他上下文发送通知,其中包含受影响对象的 ID 的对象。然后这些上下文可以使用这些对象并合并保存。

有关设置的详细说明,请查看this post.

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-02
    • 2011-02-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多