【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を使用していると以下のエラーに遭遇しました。

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

エラー発生の経緯

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

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

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

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

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

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

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

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

エラーの原因

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

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

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

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

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

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

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

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

対処法

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

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

一番簡単な方法です。

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

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

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

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

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

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

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

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

 

参考文献

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

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

スポンサードリンク