【Swift】Realmのマルチスレッド対応!freezeとthawの違い

この記事からわかること
- Realm Swiftにおけるマルチスレッド対応
- freezeやthawメソッドの使い方
- ThreadSafeReferenceとは?
index
[open]
\ アプリをリリースしました /
環境
- Xcode:16.0
- iOS:18.0
- Swift:5.9
- Realm.Swift:20.0.0
- macOS:Sonoma 14.6.1
モバイル向けのデータベースを提供しているRealm Swiftの扱いの中でマルチスレッドにおける操作方法をまとめていきます。基本的な使い方や導入方法は以下の記事を参考にしてください。
マルチスレッド対応
Realmでは異なるスレッド間に置いてオブジェクトを操作することに制約(つまりスレッドセーフではない)が設けられており、適切に管理しないとスレッド違反でアプリがクラッシュする設計になっています。
スレッドセーフになっていないのはRealmデータベースを実際に操作するための「Realm
インスタンス」と「管理下にあるオブジェクト(Managed Objects
)」です。どうやらRealmインスタンスやオブジェクトはインスタンス化時に内部的に生成されたスレッドのIDを保持しており、異なるスレッドIDで参照された場合にスレッド違反を吐くようです。
// 生成したスレッドのIDを内部的に保持
let realm = Realm()
Managed ObjectsとUnmanaged Objects
まずRealmのデータオブジェクトは「Managed Objects」と「Unmanaged Objects」に分けられます。これは実際に「Realmデータベースに保存されているもの」と「されていないもの」の違いになります。
Realmをマルチスレッドで操作するための方法はいくつか存在します。
- スレッドごとに操作
- freeze / thaw
- ThreadSafeReference
スレッドごとに操作
1つ目の方法はシンプルにスレッドごとに使用するRealmオブジェクトを切り替える方法です。Realmオブジェクトの有効範囲はインスタンス化したスレッドのみで別スレッドから操作しようとするとRealm accessed from incorrect thread.というエラーを吐きます。
Realmのインスタンス化は高速なのでスレッドごとに作成してもオーバーヘッドが少なく、パフォーマンス的にもそれほど影響はありません。
例えばスレッドA(メイン)からフェッチしたデータオブジェクトをバックグラウンドスレッドから参照したい場合はプライマリーキー(idプロパティなど)を使用して取得しなおします。この場合はプライマリーキーがスレッドセーフなプリミティブ型(StringやUUIDなど)である必要があります。
// メインスレッドで生成
let realm = try! Realm()
guard let object = realm.object(ofType: Shop.self, forPrimaryKey: "取得したいID") else { return }
// プライマリーキーをスレッドが変わる前に取得して変数に格納しておく
let id: String = object.id
DispatchQueue.global().async {
// 別スレッドでは新しくRealmインスタンスを生成
let realm = try! Realm()
guard let sameObject = realm.object(ofType: Shop.self, forPrimaryKey: id) else { return }
print("\(sameObject)")
}
freeze / thaw
2つ目の方法はfreezeメソッドを使用する方法です。freeze
は言葉通り「凍結」の意味でデータオブジェクトを操作できない状態に変化させます。これによりデータオブジェクトに対してwriteなどによる
変更などができなくなりますが、異なるスレッドに渡しても安全に値を参照することが可能になります。
let realm = try! Realm()
guard let frozenObject = realm.object(ofType: Shop.self, forPrimaryKey: "取得したいID")?.freeze() else { return }
DispatchQueue.global().async {
print("\(frozenObject)")
}
凍結したオブジェクトを元に戻したい場合はthaw
(解凍)メソッドを使用します。これによりwrite
での更新が可能になります。またthaw
メソッドは返り値がSelf?
型になります。これはfreeze
している間に対象のオブジェクトがDBから削除されていた場合にnull
になります。
let thawedObject = frozenObject.thaw()
try! realm.write {
thawedObject?.name = "New Name"
}
またDBに未保存のデータ(Unmanaged Objects)からfreeze
を呼び出すとUnmanaged objects cannot be frozen.
というエラーを吐きアプリがクラッシュするので注意してください。
ThreadSafeReference
3つ目の方法はThreadSafeReferenceクラスを使用する方法です。ThreadSafeReference
は引数に対象のオブジェクトを渡すことでインスタンス化することができ、別スレッドでresolve
メソッドの引数に自身を渡すことで対象のオブジェクトを取得できるようになります。こちらはプライマリーキーがない場合などに活用できるかと思います。
let realm = try! Realm()
guard let object = realm.object(ofType: Shop.self, forPrimaryKey: "取得したいID") else { return }
let reference = ThreadSafeReference(to: object)
DispatchQueue.global().async {
let realm = try! Realm()
if let resolvedObject = realm.resolve(reference) {
print("\(resolvedObject)")
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
参考資料:Realm accessed from incorrect thread.
ご覧いただきありがとうございました。