CoreData 和并发:无法解释的行为

Posted

技术标签:

【中文标题】CoreData 和并发:无法解释的行为【英文标题】:CoreData and Concurrency: Unexplained behavior 【发布时间】:2016-07-25 08:52:59 【问题描述】:

我听说过很多关于 CoreData 和并发性的问题。因此,我决定使用虚拟代码尝试一些场景。我无法完全解释所有观察结果。任何指针将不胜感激。

案例 1 相同的托管对象在两个不同的地方不断变化,主线程和后台线程使用下面的代码。未执行托管对象内容保存。

观察:没有崩溃。我看到“numberOfSales”的值在“主线程”和“后台队列”中读取的值不同。它们最终变得相同、分歧、变得相同等等。所以,我猜这是“最终一致性”的表现。

这是预期的行为吗?即,从多个线程对同一托管对象上下文中的对象进行更改似乎没问题。

案例 2 这两段代码还执行将托管对象上下文保存到持久存储

观察:随机崩溃。这是否意味着真正的问题是当您尝试从多个线程将内容存储到持久存储时?

案例 3 我使用串行队列序列化获取请求。如下代码示例 2 所示。

观察:没有崩溃。但我期待获取请求是串行的:一个来自主线程,一个来自后台 Q。但我看到它们中只有一个执行。为什么会这样?

在后台 Q 执行的代码块

dispatch_async(backgroundQueue, ^(void) 
            while (1) 
                sleep(1);
                NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
                NSEntityDescription *entity = [NSEntityDescription entityForName:@"Person"
                                                          inManagedObjectContext:self.managedObjectContext];
                [fetchRequest setEntity:entity];
                NSError *error;
                NSArray *fetchedObjects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
                for (Person *info in fetchedObjects) 
                    NSLog(@"BackQ Name: %@, SSN: %@, Num sales = %@", info.name,info.ssn, info.numberOfSales);
                    info.numberOfSales = @(2);
                

             //In case 1: The save to persistent store part below is commented out, in case 2: this part exists

                if (![self.managedObjectContext save:&error]) 
                    NSLog(@"Whoops, couldn't save: %@", [error localizedDescription]);
                
            
    );

在主线程中执行的代码块

while (1) 
        NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
        NSEntityDescription *entity = [NSEntityDescription entityForName:@"Person"
                                                  inManagedObjectContext:self.managedObjectContext];
        [fetchRequest setEntity:entity];
        [fetchRequest setPredicate:[NSPredicate predicateWithFormat:@"(name = %@)",@"XXX"]] ;
        NSError *error;
        NSArray <Person *>*fetchedObjects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
        fetchedObjects[0].numberOfSales = @([fetchedObjects[0].numberOfSales integerValue] + 1);
        NSLog(@"Fore Name: %@, SSN: %@, Num sales = %@", fetchedObjects[0].name,fetchedObjects[0].ssn, fetchedObjects[0].numberOfSales);

        //In case 1: The save to persistent store part below is commented out, in case 2: this part exists

        if (![self.managedObjectContext save:&error]) 
            NSLog(@"Whoops, couldn't save: %@", [error localizedDescription]);
        
    

代码示例 2

   self.coreDataQ = dispatch_queue_create("com.smarthome.coredata.bgqueue2", DISPATCH_QUEUE_SERIAL);

代码示例 2:后台代码 Q

        dispatch_async(backgroundQueue, ^(void) 
                while (1) 
                    dispatch_async(self.coreDataQ, ^(void) 
                        NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
                        NSEntityDescription *entity = [NSEntityDescription entityForName:@"Person"
                                                                  inManagedObjectContext:self.managedObjectContext];
                        [fetchRequest setEntity:entity];
                        NSError *error;
                        NSArray *fetchedObjects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
                        for (Person *info in fetchedObjects) 
                            NSLog(@"BackQ Name: %@, SSN: %@, Num sales = %@", info.name,info.ssn, info.numberOfSales);
                            info.numberOfSales = @(2);
                        
                        if (![self.managedObjectContext save:&error]) 
                            NSLog(@"Whoops, couldn't save: %@", [error localizedDescription]);
                        
                    );
                
        );

代码示例 2:主线程中的代码

   while (1) 
        dispatch_async(self.coreDataQ, ^(void) 
            NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
            NSEntityDescription *entity = [NSEntityDescription entityForName:@"Person"
                                                      inManagedObjectContext:self.managedObjectContext];
            [fetchRequest setEntity:entity];
            [fetchRequest setPredicate:[NSPredicate predicateWithFormat:@"(name = %@)",@"XXX"]] ;
            NSError *error;
            NSArray <Person *>*fetchedObjects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
            fetchedObjects[0].numberOfSales = @([fetchedObjects[0].numberOfSales integerValue] + 1);
            NSLog(@"Fore Name: %@, SSN: %@, Num sales = %@", fetchedObjects[0].name,fetchedObjects[0].ssn, fetchedObjects[0].numberOfSales);
            if (![self.managedObjectContext save:&error]) 
                NSLog(@"Whoops, couldn't save: %@", [error localizedDescription]);
            
        );
    

【问题讨论】:

不能在后台线程中直接使用objectContext,如果要实现,必须创建具有后台线程能力的Child Context。 是的,这就是我在 CoreData 编程指南中看到的理论。但我在实践中没有看到这一点。实际上在代码示例 2 中,只有“后台队列块”中的 NSLOg 正在执行。 开启并发断言,如oleb.net/blog/2014/06/core-data-concurrency-debugging所示——实际上,通读全文 【参考方案1】:

如果您将dispatch_async 用于并发Core Data 代码,那么您已经做错了。它不会立即崩溃的事实并不意味着什么。你已经越过了“警告,前方有杀手龙”的标志,没有龙吃掉你的事实并不意味着你在做安全的事情。

如果您在多个线程或队列上使用 Core Data,您必须对涉及 Core Data 的每个操作使用performBlockperformBlockAndWait以任何方式。这意味着您使用NSMainQueueConcurrencyTypeNSPrivateQueueConcurrencyType 创建了托管对象上下文。此规则只有一个例外:如果您使用了 NSMainQueueConcurrencyType 并且您确定您的代码正在从主队列运行,则不必使用 performBlockperformBlockAndWait .

分析示例代码中的流程没有用;你严重违反了 Core Data 的并发规则,所以唯一真正重要的解释是它不一致,因为你做错了。

【讨论】:

谢谢,汤姆。我只是好奇,为什么设计不能像1那么简单。如果您正在访问不同的表或者您只是在读取数据,那么可以同时形成多个线程。 3. 如果您要写入多个表,如果您通过串行队列对所有请求进行序列化,则可以这样做。而不是所有这些并发症? 因为 Core Data 在某种程度上不是线程安全的,这允许您的第 1 点,但是这个 API 允许从多个线程访问。 Core Data 的块方法通过串行队列对请求进行序列化,因此实际上并没有更复杂,只是不同而已。

以上是关于CoreData 和并发:无法解释的行为的主要内容,如果未能解决你的问题,请参考以下文章

NSArrayController 和 Core Data 的行为不符合预期

在 Core Data 中加载具有关系的托管对象时的标准行为是啥?

解释 Core Data 验证消息并在 iPhone 上显示它们的好模式是啥?

在 SwiftUI 中将 @FetchRequest 与 Core Data 一起使用时修改 nil 排序行为

NSObjectInaccessibleException',原因:'CoreData 无法完成错误。如何解释这个?

iOS swift应用程序中Core Data的奇怪行为