読者です 読者をやめる 読者になる 読者になる

甘いものが好きです

iOS App開発時に感じた疑問や課題、その他の雑感などを書いていきます。

マルチスレッドでCore Dataを利用する場合にはスレッドごとにManaged Object Contextを用意する

Managed Object Contextはスレッドセーフではない

Core Dataを利用するiOS Appの処理の一部をマルチスレッド実行するように修正したところ、実行時エラーが発生して強制終了するようになってしまった。このときデバッグコンソールには次のようなメッセージが出力された。

2012-01-13 14:09:16.441 [App名][12661:6c03] *** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSCFSet: 0x27cbd0> was mutated while being enumerated.'

ステップ実行によりエラー発生箇所を調べたところ、NSManagedObjectContextオブジェクトに対してexecuteFetchRequest:error:を送信したところでこのエラーが発生していることがわかった。複数のスレッドで同じNSManagedObjectContextオブジェクトを参照していることがエラーの原因となっているようだ。

Core Dataをマルチスレッドで使用する方法

Managed Object Contextはスレッドセーフではないため、Core Dataを複数スレッドで同時に使用したりバックグラウンドスレッドで使用するには、そのための対応を行わなければならない。『Core Data Programming Guide』*1内の「Concurrency with Core Data」という章によると、マルチスレッド対応の方法には次の2つがあるとされている。

  • スレッドごとに別のManaged Object Contextを作成してスレッド間でひとつのPersistent Store Coordinatorを共有する。(推奨)
  • スレッドごとに別のManaged Object ContextおよびPersistent Store Coordinatorを作成する。

スレッド間でManaged ObjectやManaged Object Contextの受け渡しができないことに注意が必要。スレッド間でこれらのやりとりをするためには、対象のManaged ObjectのObject-IDを利用し、スレッドに対応づけられたManaged Object Contextオブジェクトに対してobjectWithID:やexistingObjectWithID:error:といったメッセージを送信する等の対応が必要である。

スレッドごとに別のManaged Object Contextを作成して使用する

スレッドごとに別のManaged Object Contextを作成してスレッド間でひとつのPersistent Store Coordinatorを共有する方法については、Stack Overflowの次のページで具体的に説明されている。
iphone - Core Data and threads / Grand Central Dispatch - Stack Overflow
以下では、このページ内に記載されていたコードを引用する形で、マルチスレッド処理においてCore Dataを使用する際の注意点を簡単に整理しておく。

dispatch_queue_t request_queue = dispatch_queue_create("com.yourapp.DescriptionOfMethod", NULL);
dispatch_async(request_queue, ^{

    // Create a new managed object context
    // Set its persistent store coordinator
    AppDelegate *theDelegate = [[UIApplication sharedApplication] delegate];
    NSManagedObjectContext *newMoc = [[NSManagedObjectContext alloc] init];
    [newMoc setPersistentStoreCoordinator:[theDelegate persistentStoreCoordinator]];

    // Register for context save changes notification
    NSNotificationCenter *notify = [NSNotificationCenter defaultCenter];
    [notify addObserver:self 
               selector:@selector(mergeChanges:) 
                   name:NSManagedObjectContextDidSaveNotification 
                 object:newMoc];

    // Do the work
    // Your method here
    // Call save on context (this will send a save notification and call the method below)
    BOOL success = [newMoc save:&error];
    [newMoc release];
});
dispatch_release(request_queue);

マルチスレッドでCore Dataを使用する場合には、スレッドごとにコンテキスト(NSManagedObjectContext)オブジェクトを作成してそのコンテキストを使用する。各コンテキストには共通のNSPersistentStoreCoordinatorオブジェクトを設定する。
そのコンテキストにおける変更分を保存するタイミングで、メインのNSManagedObjectContextオブジェクトがマージ処理を実行する。保存処理の完了はNSManagedObjectContextDidSaveNotificationという通知により知ることができるので、この通知についての監視を行う必要がある。

- (void)mergeChanges:(NSNotification*)notification 
{
    AppDelegate *theDelegate = [[UIApplication sharedApplication] delegate];
    [[theDelegate managedObjectContext] performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];
}

マージ処理はメインのコンテキストにmergeChangeFromContextDidSaveNotification:を送信することにより実行される。mergeChangeFromContextDidSaveNotification:の引数には、NSManagedObjectContextDidSaveNotificationの通知を受け取った時に渡されたNSNotificationオブジェクトを使用する。

[[NSNotificationCenter defaultCenter] removeObserver:self];

スレッドの処理が終わった時点で忘れずに通知監視を終える。

*1:2011年8月11日更新版にて確認。