【Swift】「Terminating app due to uncaught exception RLMException, reason: Realm accessed from incorrect thread.」のエラーでiOSアプリがクラッシュする事象【備忘録】【Xcode】

2022年6月3日

iOS アプリ開発でよく使うデータベースといえば、RealmSwift が代表的だと思います。

(この記事を書くまで知らなかったのですが、2019 年に Realm は MongoDB に買収されていたんですね。ドキュメントも以下の通り刷新されています)

Welcome to the Realm Docs

本題ですが、Realmを使用していると以下のエラーに遭遇しました。

Terminating app due to uncaught exception 'RLMException', reason: 'Realm accessed from incorrect thread.'

今回はこのエラーの原因と、私が対処した方法を残しています。

エラー発生の経緯

「高頻度でDBにアクセスする」という要件を実現する必要がありました。

メインスレッドで高頻度にDBアクセスを行うとUIがフリーズしてしまうので、サブスレッドからDBアクセスを行う処理を書いていました。

幸い、RealmはサブスレッドからDBにアクセスできます。

今回はバックグラウンド処理を実現するために、DispatchQueue を使用しました。

サンプルコードは以下の通りです。


class ViewController: UIViewController {

    // Realmのインスタンス
    var realm: Realm!
    // バックグラウンドで処理を行うためのDispatchQueue
    let queue = DispatchQueue(label: "sample")
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 1秒間に60回処理を実行するためのタイマーを作成
        let displayLink = CADisplayLink(target: self, selector: #selector(update(_:)))
        displayLink.preferredFramesPerSecond = 60
        displayLink.add(to: .main, forMode: .common)
    }
    
    // 以下の処理が1秒間に60回呼び出される
    @objc func update(_ displayLink: CADisplayLink) {
        // DispatchQueue に処理を登録
        queue.async {
            // Realmのインスタンスを初期化
            if self.realm == nil {
                do {
                    self.realm = try Realm()
                } catch {
                    print(error)
                }
            }
            // Realm からオブジェクトを取り出す
            // Error! -> ここで掲題のエラーが発生
            let sample = self.realm.objects(Sample.self)
        }
    }
}

プライベートな DispatchQueue を一つ定義して、同じQueueにDBアクセスの処理を登録しています。

Realmインスタンスは一度だけインスタンス化し、以降は使い回すようにしていました(初期化コストを削減するため)。

単一のQueueを介してRealmインスタンスを使い回しているので、Realmの「スレッドセーフではない」という制約を回避できると思っていました。

エラーの原因

そもそもの勘違いだったのが、上で赤く記載した箇所です。

同じDispatchQueueを使用しているからといって、毎回同じスレッドが使用されるとは限らない

大前提として、iOSではスレッドの管理はOSが行なっています。(Androidのようにスレッドを自分で作成することはできない)

DispatchQueueに処理を登録すると、「OSが管理しているサブスレッドプールからスレッドが一つ割り当てられて、タスクが実行される」という動きになります。(割り当てられるスレッドは実行時にOSが決める)

試しに、DispatchQueue に登録したタスクの実行時に、現在のスレッドをプリントしてみました。


    @objc func update(_ displayLink: CADisplayLink) {
        queue.async {
            // ここで現在のスレッドを出力
            print(Thread.current)
            if self.realm == nil {
                do {
                    self.realm = try Realm()
                } catch {
                    print(error)
                }
            }
            let sample = self.realm.objects(Sample.self)
        }
    }

コンソールの出力は以下の通りです。

<NSThread: 0x6000037ed480>{number = 3, name = (null)}
<NSThread: 0x6000037ed480>{number = 3, name = (null)}
<NSThread: 0x6000037ed480>{number = 3, name = (null)}
<NSThread: 0x6000037ed480>{number = 3, name = (null)}
<NSThread: 0x6000037ed480>{number = 3, name = (null)}
<NSThread: 0x6000037ed480>{number = 3, name = (null)}
<NSThread: 0x6000037fb7c0>{number = 6, name = (null)}
---- *** Terminating app due to uncaught exception 'RLMException', reason: 'Realm accessed from incorrect thread.'

最初の数回は、スレッド番号「3」が使われていますが、エラー発生時はスレッド番号「6」が使われています。

Realmはスレッドセーフではないので、「Realmインスタンスを初期化したスレッド以外から参照されてるよ!」というエラーが発生している、というのが今回の問題です。(Realmインスタンスを初期化したスレッドは「スレッド番号:3」なので、このスレッド以外から参照するとエラーが発生)

対処法

対処法はいくつかあると思いますが、本記事では2つの対処法を紹介します。

毎回Realmインスタンスを初期化する

一番簡単な方法です。

Realmインスタンスを使いまわさずに、必要になったタイミングで毎回生成します。


    do {
        // DBアクセスのたびに毎回初期化
        let realm = try Realm()
        let sample = realm.objects(Sample.self)
    }

通常のユースケースだと、初期化コストはそれほど影響はないと思われるので、こちらで問題ないでしょう。

ただ、もし先ほどの例のように「高頻度にDBアクセスが発生する」という場合は、普段は気にならない初期化コストが積み重なって無視できなくなることもあります。

スレッドごとにRealmインスタンスを作成する

Realmインスタンスはスレッドセーフではないので、「スレッドごとにRealmインスタンスを作成する」という強引なやり方です。

まず以下のような、Realmインスタンスと紐づくスレッドを保持する型を作成します。

struct RealmData {
    // Realmインスタンス
    var realm: Realm
    // スレッドの参照
    weak var thread: Thread?
}

そして、現在のスレッドごとに、紐づくRealmインスタンス取り出してDBアクセスを実行します。

class ViewController: UIViewController {
    // 「Realmインスタンスと紐づくスレッド」を格納するリストを定義
    var realmDataList = [RealmData]()

    // ..省略..

    @objc func update(_ displayLink: CADisplayLink) {
        queue.async {
            // 現在のスレッドに紐づくRealmインスタンスを取得する
            var realm = self.realmDataList.first(where: { $0.thread == Thread.current })?.realm
            // スレッドに紐づくRealmインスタンスがなければ新規作成
            if realm == nil {
                do {
                    realm = try Realm()
                    // 現在のスレッドに紐づくRealmインスタンスを保存する
                    self.realmDataList.append(RealmData(realm: realm!, thread: Thread.current))
                } catch {
                    print(error)
                    return
                }
            }
            // DBアクセス
            let sample = realm!.objects(Sample.self)
        }
    }
}

上記のコードはクラッシュすることなく動作します。

 

参考文献

https://stackoverflow.com/questions/41781775/realm-accessed-from-incorrect-thread-again

https://qiita.com/reo0612/items/8a373fdd2b9294773550