【问题标题】:Core Data Multithreading Import (Duplicate Objects)核心数据多线程导入(重复对象)
【发布时间】:2013-08-17 00:50:27
【问题描述】:

我有一个 NSOperationQueue,它将对象导入到我从 Web api 获得的核心数据中。每个操作都有我应用的主 managedObjectContext 的私有子 managedObjectContext。每个操作都获取要导入的对象并检查该对象是否已经存在,在这种情况下它会更新现有对象。如果对象不存在,它会创建这个新对象。然后将私有子上下文上的这些更改传播到主托管对象上下文。

此设置对我来说效果很好,但存在重复问题。

当我在两个不同的并发操作中导入相同的对象时,我会得到具有完全相同数据的重复对象。 (他们都检查对象是否存在,并且在他们看来它并不存在)。我将大约同时导入 2 个相同对象的原因是我经常处理“新”api 调用以及“get”api 调用。由于我的设置的并发异步性质,很难确保我永远不会尝试导入重复的对象。

所以我的问题是解决这个特定问题的最佳方法是什么?我考虑过将最大并发操作的导入限制为 1(由于性能,我不喜欢这个)。同样,我考虑在每次导入操作后都需要保存并尝试处理上下文的合并。此外,我考虑过在事后整理数据以偶尔清理重复项。最后,我考虑只处理所有获取请求的重复项。但这些解决方案对我来说都不是很好,也许有一个简单的解决方案我已经看过了。

【问题讨论】:

  • 很好的问题。我也面临同样的问题,正要自己问这个问题。

标签: objective-c multithreading core-data nsmanagedobjectcontext nsoperation


【解决方案1】:

首先在您的操作之间创建依赖关系。确保在其依赖项完成之前无法完成。

查看http://developer.apple.com/library/mac/documentation/Cocoa/Reference/NSOperation_class/Reference/Reference.html#//apple_ref/occ/instm/NSOperation/addDependency:

每个操作都应该在完成时调用 save。接下来,我会尝试这里建议的 Find-Or-Create 方法:

https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreData/Articles/cdImporting.html

它将解决您的重复问题,并且可能会导致您执行更少的提取(这既昂贵又缓慢,因此会很快耗尽电池电量)。

您还可以创建一个全局子上下文来处理所有导入,然后在最后合并整个巨大的东西,但这实际上归结为数据集的大小和您的内存考虑。

【讨论】:

  • 所以我相信我正在遵循 Find-Or-Create 方法中建议的模式。我的重复问题只会出现,因为我同时进行其中两个导入。我应该更清楚,但我的设置中的单个操作可能是处理特定对象的整个数组。例如,一个操作将处理从“get”请求返回的数组,而另一个操作将处理从“new”请求返回的单个对象。
  • 参见上面的编辑,但我认为如果您希望一次能够处理多个项目,则在您的 NSOperation 子类上使用 addDependency: 至关重要。当然,您可以只减少 1 个并发操作,但您真正遇到的是依赖问题。
  • 所以您建议将导入相同实体类型的操作作为依赖操作?当对象属于不同类型时,这仍然允许并发操作,但需要一个可能出现问题的顺序?我想我喜欢这个建议。我将对此进行更多探索。感谢您的建议!
  • 因此,使用依赖操作解决方案的一个缺点是,如果您要导入更复杂的对象(具有其他对象类型关系的对象),则必须另外遍历这些关系以确定操作依赖。这可能会开始变得很棘手。
  • 关于您的全局子上下文建议。我认为这可能有问题,因为托管对象上下文不是线程安全的。而且您将从多个线程访问相同的上下文。请记住,每个操作都在不同的线程上。 (除非我弄错了,在这种情况下,这个解决方案可能会解决我的确切问题。)
【解决方案2】:

所以问题是:

  • 上下文是一个暂存器 - 除非您保存,否则您在其中所做的更改不会推送到持久存储;
  • 您希望一个上下文知道对另一个尚未推送的更改。

在我看来,上下文之间的合并听起来并不可行——上下文不是线程安全的。因此,要发生合并,其他上下文的线程/队列上不会有其他任何事情。因此,您永远无法消除插入新对象而另一个上下文正在插入过程中途的风险。

补充意见:

  • SQLite 在任何实际意义上都不是线程安全的;
  • 因此,无论您如何发出它们,所有到持久存储的行程都会被序列化。

考虑到问题和 SQLite 限制,在我的应用程序中,我们采用了一个框架,根据 NSURLConnection,网络调用自然是并发的,随后解析结果(JSON 解析加上一些钓鱼结果)同时发生,然后将查找或创建步骤引导到串行队列中。

序列化损失的处理时间非常少,因为无论如何 SQLite 行程都会被序列化,而且它们是序列化内容的绝大多数。

【讨论】:

  • 是的,这是有用的信息。我已经通过我如何设置我的核心数据堆栈解决了您所说的一些 SQL 问题。我有一个 NSPrivateQueueConcurrencyType 类型的私有上下文,这个上下文的唯一工作是写入持久存储。在此上下文中,我有一个 NSMainQueueConcurrencyType 类型的子上下文,我将其用作我的应用程序的主要上下文。这个设置的美妙之处在于我可以控制何时写入我的持久存储。如果有人感兴趣,我的设置遵循此设置:cocoanetics.com/2012/07/multi-context-coredata
  • 在这个思路上,这并不是该功能的真正用途,但我发现当应用程序退出活动并在那里执行昂贵的阻塞核心数据工作时使用UIApplication -beginBackgroundTaskWithExpirationHandler:从苹果的角度来看是完全可以接受的。只要确保它是可中断的,以防您的应用程序再次激活。这就是我们进行删除的地方。如果您能够推迟写入磁盘,那么这可能是一个非常好的机会。
  • 这是一个绝妙的建议!我没有想到要删除这个。谢谢。
【解决方案3】:

我已经为同样的问题苦苦挣扎了一段时间。到目前为止关于这个问题的讨论给了我一些想法,我现在将分享这些想法。

请注意,这基本上是未经测试的,因为在我的情况下,我在测试期间很少看到这个重复的问题,而且我没有明显的方法可以轻松地重现它。

我有相同的 CoreData 堆栈设置 - 私有队列上的主 MOC,主队列上有一个子队列,它用作应用程序的主上下文。最后,批量导入操作(查找或创建)使用后台队列传递到第三个 MOC。操作完成后,保存将传播到 PSC。

我已将我的所有核心数据堆栈从 AppDelegate 移动到一个单独的类 (AppModel),该类为应用程序提供对域的聚合根对象 (Player) 的访问权限,以及一个帮助函数对模型执行后台操作 (performBlock:onSuccess:onError:)。

对我来说幸运的是,所有主要的 CoreData 操作都是通过这种方法汇集的,所以如果我能确保这些操作是串行运行的,那么重复问题应该得到解决。

- (void) performBlock: (void(^)(Player *player, NSManagedObjectContext *managedObjectContext)) operation onSuccess: (void(^)()) successCallback onError:(void(^)(id error)) errorCallback
{
    //Add this operation to the NSOperationQueue to ensure that 
    //duplicate records are not created in a multi-threaded environment
    [self.operationQueue addOperationWithBlock:^{

        NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        [managedObjectContext setUndoManager:nil];
        [managedObjectContext setParentContext:self.mainManagedObjectContext];

        [managedObjectContext performBlockAndWait:^{

            //Retrive a copy of the Player object attached to the new context
            id player = [managedObjectContext objectWithID:[self.player objectID]];
            //Execute the block operation
            operation(player, managedObjectContext);

            NSError *error = nil;
            if (![managedObjectContext save:&error])
            {
                //Call the error handler
                dispatch_async(dispatch_get_main_queue(), ^{
                    NSLog(@"%@", error);
                    if(errorCallback) return errorCallback(error);
                });
                return;
            }

            //Save the parent MOC (mainManagedObjectContext) - WILL BLOCK MAIN THREAD BREIFLY
            [managedObjectContext.parentContext performBlockAndWait:^{
                NSError *error = nil;
                if (![managedObjectContext.parentContext save:&error])
                {
                    //Call the error handler
                    dispatch_async(dispatch_get_main_queue(), ^{
                        NSLog(@"%@", error);
                        if(errorCallback) return errorCallback(error);
                    });
                    return;
                }
            }];

            //Attempt to clear any retain cycles created during operation
            [managedObjectContext reset];

            //Call the success handler
            dispatch_async(dispatch_get_main_queue(), ^{
                if (successCallback) return successCallback();
            });
        }];
    }];
}

我在这里添加的内容是我希望能够为我解决问题的内容是将整个内容包含在addOperationWithBlock 中。我的操作队列简单配置如下:

single.operationQueue = [[NSOperationQueue alloc] init];
[single.operationQueue setMaxConcurrentOperationCount:1];

在我的 API 类中,我可能会按如下方式对我的操作执行导入:

- (void) importUpdates: (id) methodResult onSuccess: (void (^)()) successCallback onError: (void (^)(id error)) errorCallback
{
    [_model performBlock:^(Player *player, NSManagedObjectContext *managedObjectContext) {
        //Perform bulk import for data in methodResult using the provided managedObjectContext
    } onSuccess:^{
        //Call the success handler
        dispatch_async(dispatch_get_main_queue(), ^{
            if (successCallback) return successCallback();
        });
    } onError:errorCallback];
}

现在有了NSOperationQueue,应该不再可能同时进行多个批处理操作。

【讨论】:

  • 是的,这是针对我遇到的特定问题的已确认解决方案。对我来说,不允许队列有超过 1 个并发操作对性能的影响太大了。但对于很多人来说,这可能是一个很好的解决方案。
猜你喜欢
  • 2013-03-02
  • 2011-02-07
  • 2020-12-24
  • 1970-01-01
  • 1970-01-01
  • 2012-01-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多