Site icon image技術メモ

[scraps]Realmをスレッドセーフに扱う

📝
追記
  • Realmは別スレッドからアクセスすると普通にクラッシュするので、Concurrency対応してスレッドセーフな状態を設計で保証する価値があるのではないか

~もろもろ調査後↓~

  • このIssueがある時点で、完全なConcurrency対応を行うのが現実的ではない
  • I/O処理のスレッドを縛ってスレッド安全な形にしよう

📝
追記
  • 方針
    • GlobalActorを定義して全体的に付与するだけ
    • 読み込み並列処理など検討したが、Concurrecny本対応するタイミングで考えればいいかな

📝
追記
  • 一旦PR作成Done
  • やったことを以下にまとめる

  • 実装の要点
    • 既存 RealmWrapper のインターフェースを asyncに変更
    • GlobalActor RealmActor の作成
      • なぜactor型にするのではなくGlobalActorを用いるのか
        • 複数のRealmWrapperインスタンスが存在していても、データベースは一つなので、データベース操作が直列で実行されるようにしたい
        • actor型を作ってsingleton化するやり方もあるが、より扱いやすいので
    • RealmWrapperRealmActor 付与
      • GlobalActorに隔離された型として、Sendableになる
    • マネージドオブジェクトがActor境界を超えてアクセスされないように対策する
      • マネージドオブジェクトとは?
        • Realmによって保持されているオブジェクト
          • Realmデータベースから取得したオブジェクトはマネージドオブジェクト
          • 新規作成し、データベースに保存していない状態のオブジェクトはアンマネージドオブジェクト
      • 対策内容
        • RealmObjectとEntityの変換処理をclosureとしてRealmWrapperの関数の引数に追加
          // Interface
          func getConvertedObjects<O: Object, V: Sendable>(
              conversion: @escaping @Sendable (O) -> V?
          ) async -> [V]
          
          // Implementation
          public func getConvertedObjects<O: Object, V: Sendable>(
              conversion: @escaping @Sendable (O) -> V?
          ) async -> [V] {
              guard let results: Results<O> = realm?.objects(O.self) else {
                  return []
              }
              return results.compactMap { result in
                  let converted: V? = conversion(result)
                  return converted
              }
          }
              
          // Caller
          let conversion: @Sendable (WaReadStatusObject) -> WaReadStatus = { @Sendable (object) in
              WaReadStatus(titleId: object.titleId, volumeNo: object.volumeNo, fileNo: object.fileNo, isRead: object.isRead)
          }
          
          let results: [WaReadStatus] = await realmWrapper.getConvertedObjects(conversion: conversion)
          
    • その他補足事項
      • Realmインスタンス生成時の工夫
        • Realm生成時に、引数にActorを与えることで、Realmが属するActorを指定することができる
        • その場合処理がasyncになる
        • しかし、それに影響される形でRealmWrapperのinitをasyncにすると、DI層にも影響がでるので避けたい
        • 結果、以下のような生成処理をデータベースアクセスする前に実行する形に落ち着いた
          private func injectRealmInstanceIfNeeded() async {
              guard realm == nil else {
                  return
              }
              realm = await {
                  do {
                      let config = Realm.Configuration(schemaVersion: 2)
                      let realm = try await Realm(configuration: config, actor: RealmActor.shared)
                      Logger.log.debug("Realm initialized successfully, thread: \(Thread.current.description)")
                      return realm
                  } catch let error as NSError {
                      fatalError(error.localizedDescription)
                  }
              }()
          }
        • 以下のように、init内でTask生成し実行することも検討したが、競合状態が発生した
          public nonisolated init() {
              Task(priority: .high) { @RealmActor in
                  await injectRealmInstanceIfNeeded()
              }
          }