核心数据多线程:代码示例

3

我一直在使用支持多线程的Core Data应用程序时遇到了问题,因此我认为我应该仔细检查一下我的操作和方式。请告诉我以下内容是否可行。

我有一个单例DataManager类来处理Core Data相关的事务。它有一个属性managedObjectContext,为每个线程返回不同的MOC。因此,给定NSMutableDictionary *_threadContextDict(将字符串线程名称映射到上下文)和NSMutableDictionary *_threadDict(将字符串线程名称映射到线程),它看起来像这样:

-(NSManagedObjectContext *)managedObjectContext
{
  if ([NSThread currentThread] == [NSThread mainThread])
  {
    MyAppDelegate *delegate = [[UIApplication sharedApplication] delegate];
    return delegate.managedObjectContext; //MOC created in delegate code on main thread
  }
  else
  {
    NSString *thisThread = [[NSThread currentThread] description];
    {
      if ([_threadContextDict objectForKey:thisThread] != nil)
      {
        return [_threadContextDict objectForKey:thisThread];
      }
      else
      {
        NSManagedObjectContext *context = [[NSManagedObjectContext alloc]init];
        MyAppDelegate *delegate = [[UIApplication sharedApplication] delegate];
        [context setPersistentStoreCoordinator:delegate.persistentStoreCoordinator];
        [_threadContextDict setObject:context forKey:thisThread];
        [_threadDict setObject:[NSThread currentThread] forKey:thisThread];

        //merge changes notifications
        NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
        [center addObserver:self selector:@selector(mergeChanges:)
            name:NSManagedObjectContextDidSaveNotification object:context];

        return context;
      }
    }
  }
}

mergeChanges方法中,我将来自传入通知的更改合并到除生成通知的上下文之外的所有上下文中。代码如下:
-(void)mergeChanges:(NSNotification *)notification
{
  MyAppDelegate *delegate = [[UIApplication sharedApplication] delegate];
  NSManagedObjectContext *context = delegate.managedObjectContext;
  [context performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification)
     withObject:notification waitUntilDone:YES];

  for (NSString *element in [_threadContextDict allKeys])
  {
    if (![element isEqualToString:[[NSThread currentThread] description]])
    {
      NSThread *thread = [_threadDict objectForKey:element];
      NSManagedObjectContext *threadContext = [_threadContextDict objectForKey:element];
      [threadContext performSelector:@selector(mergeChangesFromContextDidSaveNotification)
         onThread:thread withObject:notification waitUntilDone:YES];
    }
  }
}

每当我保存 MOC 上的更改时,都会调用共享的 DataManager 上的 saveContext 方法,该方法调用从上述属性获取的上下文上的 save 方法:
-(void)saveContext
{
  NSManagedObjectContext *context = self.managedObjectContext;
  NSError *err = nil;
  [context save:&err];
  //report error if necessary, etc.
}

根据我对核心数据多线程规则的理解,我觉得这应该可以工作。我为每个线程使用单独的上下文,但所有线程都使用相同的持久存储。但是,当我使用它时,即使我的线程不在同一对象(NSManagedObject子类)上工作,我仍然会遇到许多合并冲突。我只是从网络下载数据、解析结果并将它们保存到Core Data中。我做错了什么吗?我尝试过使用NSLock实例锁定某些内容,但那样我只会遇到卡顿问题。
更新/解决方案:通过添加一件简单的事情,我成功地使其工作。当完成一个线程/MOC对时,我添加了一种方法来从字典中删除它。在每次调用dispatch_async中的每个块的末尾,我调用[self removeThread],这会从字典中删除当前线程及其MOC。我也只合并对主线程MOC的更改。实际上,这意味着每次在后台线程上工作时,我都会获得一个全新的MOC。
我还通过向userInfoDict添加数字来区分线程,而不是调用description。该数字是通过我的类上的只读属性获得的,每次调用它时返回一个更高的数字。

你接受了一个答案,但是你能解决你的问题吗?你做了什么? - J2theC
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreData/Concurrency.html - James Bush
1个回答

7
尊重您的方法是一场噩梦,如果有问题需要进行调试,那么情况可能会更糟。首要问题是:我有一个单例DataManager。不要使用单例对象来管理在不同线程上操作核心数据的不同实体。单例在处理多线程环境时很棘手,特别是在核心数据中使用它甚至更糟糕。
第二件事,不要使用NSThread来处理多线程。有更现代的API可用。使用Grand Central Dispatch或NSOperation/NSOperationQueue。自从引入块(iOS 4)以来,苹果已经鼓励人们从NSThread转移。并且为了将来的参考,请勿像您正在使用的方式一样使用对象的描述。描述通常/大多数用于调试目的。那里的信息不应该用于比较。甚至不是指针值(这就是为什么你应该使用isEqual而不是==)。
这是关于核心数据和多线程的知识:
1.为每个线程创建一个上下文。核心数据模板已经为您创建了一个主线程上下文。在执行后台线程的开始时(在块内部或在NSOperation子类的主方法中),初始化您的上下文。
2.一旦初始化了您的上下文,并拥有正确的persistentStoreCoordinator,请侦听NSManagedObjectContextObjectsDidChangeNotification。监听通知的对象将在保存上下文的同一线程中接收通知。由于这与主线程不同,因此需要在使用接收上下文的线程上使用合并上下文进行合并调用。假设您正在使用一个线程内的上下文,该线程与主线程不同,并且您想要与主线程合并,则需要在主线程内调用合并方法。可以使用dispatch_async(dispatch_get_main_queue(), ^{ // code here });来执行此操作。
3.不要在其managedObjectContext所在的线程之外使用NSManagedObject。
通过这些和其他简单规则,在多线程环境下管理核心数据更加容易。您的方法更难以实现,更难以调试。对体系结构进行一些更改。根据您正在处理的线程来管理上下文(而不是集中式)。不要在超出其范围的情况下保留对内容的引用。一旦创建了第一个上下文,创建同样的上下文并不昂贵,只要在同一块/NSOperation执行内即可重用相同的上下文。

1
谢谢你的诚实。虽然我不明白为什么我的方法没有遵守那些规则。我的意图是将这些规则抽象出来,这样我就不必在代码中一直担心这些规则。我使用NSThread,因为我假设我需要跨所有上下文合并更改(我错了吗?),而这必须在每个上下文的线程上完成。我还没有找到使用GCD指定块应在哪个线程上运行的方法,除了主线程的dispatch_get_main_queue() - Tom Hamming
首先,您不应该保留引用以访问不属于您的线程,甚至不是队列。如果您想要一个队列,请使用dispatch_queue_create("com.aname.aname", DISPATCH_QUEUE_SERIAL或DISPATCH_QUEUE_CONCURRENT)创建自己的队列。要获取后台队列,可以使用dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{//code here});。如果您的上下文寿命较短,则保持同步更容易。请查看相关文档信息: - J2theC
除非您重新获取或重新故障对象,否则在一个上下文中对托管对象所做的更改不会传播到另一个上下文中的相应托管对象。如果您需要在一个线程中跟踪对另一个线程中托管对象所做的更改,则有两种方法可供选择,均涉及通知。为了说明目的,请考虑两个线程“A”和“B”,并假设您想要从B向A传播更改。 - J2theC

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接