Zenn
🗝️

swift-concurrency-extrasのLockIsolatedについて

2024/12/06に公開

TCA Advent Calendar 2024の記事です。

はじめに

TCAに使われているswift-concurrency-extrasLockIsolatedについて解説します。バージョンはv1.3です。

LockIsolatedについて

めちゃくちゃ大雑把にいうと、LockIsolatedはロック付きのスレッドセーフ用ラッパーでダイナミックメンバールックアップを利用する仕組みを提供してくれています。

単にスレッドセーフにするラッパーなのではなく、ダイナミックメンバールックアップを利用しているのが興味深いところです。

テストコード

LockIsolatedのテストコードをみて理解してみます。テストコードの記述順はシンプルなものを先に紹介するようにしています(実際のテストの記述順とこの紹介順は違います)。

testInitializationWithValue

イニシャライザで値を初期化時にセットし、それがvalueプロパティで取得できるかをテストしてます。単純。

    func testInitializationWithValue() {
      let value = 10
      // LockIsolatedを初期化
      let lockIsolated = LockIsolated(value)
      // 初期値が期待通りであることを確認
      XCTAssertEqual(lockIsolated.value, value)
    }

testInitializationWithClosure

イニシャライザはクロージャでもあるのでそのクロージャのテストをしてます。1+1に特に意味はないと思います。

    // testInitializationWithClosureメソッドのテスト
    // LockIsolatedを初期化し、1 + 1の結果を設定
    func testInitializationWithClosure() {
      let lockIsolated = LockIsolated<Int>(
        1 + 1
      )
      // 初期値が期待通りであることを確認
      XCTAssertEqual(lockIsolated.value, 2)
    }

1+1は値になるので、それより定数として外でクロージャを書いて渡す方がクロージャっぽいかなと思います。

testLockThreadSafety

iterations回数を非同期にインクリメントを繰り替えし、それがちゃんとデータ競合せずにiterationsの値になっているかを検証してます。

    func testLockThreadSafety() {
      // LockIsolatedの初期値を0に設定
      let value = LockIsolated(0)
      // 繰り返し回数を設定
      let iterations = 100_000
      // 非同期処理のグループを作成
      let group = DispatchGroup()

      // 値をインクリメントする非同期タスクを作成
      for _ in 1...iterations {
        group.enter() // グループにタスクを追加
        DispatchQueue.global().async {
          // LockIsolatedの値を安全に更新
          value.withValue { value in
            value += 1 // 値を1増加
          }
          // タスク完了を通知。
          // enterの数とあってればいいだけ。
          group.leave()
        }
      }

      // 値を読み取る非同期タスクを作成
      // しかし別にこのコードがなくてもいい気がする...。
      for _ in 1...iterations {
        group.enter() // グループにタスクを追加
        DispatchQueue.global().async {
          _ = value.value
          group.leave() // タスク完了を通知
        }
      }

      group.wait() // すべてのタスクが完了するのを待機
      XCTAssertEqual(value.value, iterations)
    }

これがロックがないとインクリメントしようとする元の値が古かったりするため、基本的なデータ競合を起こすのだが、それを避けられていてスレッドセーフだよというのを示してます。スレッドセーフかをチェックするテストコードとして定番という感じです。

testDynamicMemberLookup

ダイナミックメンバールックアップのテストをしてます。

    func testDynamicMemberLookup() {
      // ダイナミックメンバールックアップを試すため、
      // x, yプロパティを持った型を用意。
      struct TestValue: Sendable {
        var x = 0
        var y = 10
      }

      let testValue = TestValue()
      let lockIsolated = LockIsolated(testValue)
    // ダイナミックメンバールックアップを検証のため、
      // lockIsolatedにx,yプロパティでアクセスしてる。
      XCTAssertEqual(lockIsolated.x, testValue.x)
      XCTAssertEqual(lockIsolated.y, testValue.y)
    }

これが一番重要です。

testSetValue

setValueメソッドのテストをしている。

    func testSetValue() {
      let initialValue = 0
      let lockIsolated = LockIsolated(initialValue)
      lockIsolated.setValue(2)
      XCTAssertEqual(lockIsolated.value, 2)
    }

これもシンプルです。Setというメソッドを明示的に使う方法もあるよというだけのことですが。

おわりに

LockIsolatedはTCAにも使われますが、単体でも使いたい時があるかもしれません。

Discussion

ログインするとコメントできます