【Swift】Core Dataをマルチスレッドで扱う方法!クラッシュチェック方法
この記事からわかること
- Swift UIでCore Dataを利用する方法
- マルチスレッドでの使い方
- Managed Object Context(MOC)の役割と定義方法
- -com.apple.CoreData.ConcurrencyDebug 1とは?
- アプリがクラッシュ(EXC_BAD_ACCESS)する際の原因と解決方法
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
環境
- Xcode:15.0.1
- iOS:17.0
- Swift:5.9
- macOS:Sonoma 14.1
Core Dataはスレッドセーフではない?
参考文献:公式リファレンス:Core Data プログラミングガイド〜Concurrency〜
Core Dataに限らず長時間かかる処理や大量のデータを処理する時などメインスレッドで実行したくない(UIをブロックしたくない)場合にはスレッドを分けて処理を実装することも多いと思います。しかしCore DataのContext(NSManagedObjectContext)はスレッドセーフではありません。そのためデータの参照や追加などをメインやバックグラウンドなど異なるスレッドから操作するとアプリがクラッシュする可能性があります。
解決方法としては「スレッドごとにContext(NSManagedObjectContext)を作成しスレッドごとのNSPersistentContainerを作成する」方法が公式で推奨されています。クラッシュを解決したいだけで、パフォーマンスに影響がなさそうならスレッドを統一するのも1つの手かもしれません。
マルチスレッド解決方法
- スレッドごとにContextを作成しスレッドごとのNSPersistentContainerを作成する
- スレッドを統一する
スレッドセーフとは?
そもそもスレッドセーフとはマルチスレッドが有効でない実行環境で、特定の処理を複数のスレッドで並行して実行しても、問題が生じない仕様や設計になっていることを指します。SwiftではGCD(Grand Central Dispatch)と呼ばれるマルチスレッド処理を行うための機能が提供されています。
Core Dataのマルチスレッド対応方法
参考文献:【Swift】Core Dataをバックグラウンドで使う
参考文献:Core Data and Concurrency
マルチスレッドに対応したContextを生成する方法は上記の記事たちを参考にさせていただきました。その方法は大きく分けて3つあるようです。
- NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
- newBackgroundContextメソッド
- performBackgroundTaskメソッド
NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
NSManagedObjectContext
のイニシャライザを使用してスレッドごとのContextを生成することができます。引数concurrencyType
には生成したいContext
の種類NSManagedObjectContextConcurrencyType
型で指定します。
mainQueueConcurrencyType
メインスレッドで動作するContext
を生成します。
let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
privateQueueConcurrencyType
バックグラウンドスレッドで動作するContext
を生成します。
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
この方法で生成したContext
ではデータを永続化させるためにpersistentStoreCoordinator
またはparent
プロパティに適切な設定する必要があり、両方とも設定することは想定されていないようです。
context.persistentStoreCoordinator = persistentContainer.persistentStoreCoordinator
// または
context.parent = makeBackgroundContext()
persistentStoreCoordinator
を設定した場合はそのContext
でsave
が呼ばれた際に、データはすぐに永続化されますが、parent
に親のContext
を指定している場合は子のContext
のsave
は親にマージされ、親のContext
でsave
が呼ばれた際に永続化されるようです。
newBackgroundContextメソッド
NSPersistentContainer
のnewBackgroundContext
メソッドを使用することで簡単にバックグラウンド用のコンテキストを取得することも可能です。実装方法や使い方などは以下の記事を参考にしてください。
persistentContainer.newBackgroundContext()
performBackgroundTaskメソッド
NSPersistentContainer
のperformBackgroundTask
メソッドを使用することでバックグラウンド用のコンテキストの生成と処理をまとめて実装することが可能です。ここで生成、使用したContext
はクロージャーを抜けると破棄されるため単発で処理を行いたい時に活用できます。
persistentContainer.performBackgroundTask { context in
// バックグラウンドで行いたい処理
}
使い訳
NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
親子関係を持たせたContextを生成したいとき
newBackgroundContextメソッド
シンプルに再利用できるバックグラウンドContextを生成したいとき
performBackgroundTaskメソッド
単発の処理だけバックグラウンドで行うためその時限りのContextを生成したいとき
マルチスレッドになっていないかチェックする方法
Core Dataを導入しているプロジェクトでマルチスレッドでコンテキストが利用されているかをチェックするためにはXcodeのスキームの引数に-com.apple.CoreData.ConcurrencyDebug 1
を追加して有効にした状態でビルドします。
アプリを操作すると異なるスレッドでContextが参照された際にEXC_BREAKPOINT
などのエラーが発生して教えてくれるようになりました。
マルチスレッドで実行すると発生するエラー
マルチスレッドでコンテキストが利用されている場合に必ずエラーが発生するわけではないようで、正常に動作する場合やアプリがクラッシュする場合など状況に応じて変化するようです。
クラッシュはしないがXcodeのログに以下のようなエラーが表示されたり
CoreData: error: NULL _cd_rawData but the object is not being turned into a fault
以下のエラーが発生しアプリがクラッシュすることもありました。
EXC_BAD_ACCESS (code=1, address=0xb91088020)
ご覧いただきありがとうございました。