🔒
SwiftのMutexで、並行安全にミュータブルで同期的なクラスプロパティを使う
概要
iOS 18, macOS 15以降でMutexというAPIがSwiftで利用可能になったため、その使い方の紹介です。
ミュータブルなクラスプロパティは並行安全ではない
Swift6で並行安全に対するコンパイラチェックが入ったことで、アクター境界を超える場合にミュータブルなプロパティを持つクラスなどは使用できなくなったのですが、以下は無理やりコンパイラを黙らせて並行安全でないことを示すコードです。
1万回、num += 1を並行に実行しています。結果としてnumは10000になっておらず、9652になっているため、並行処理が競合して何回か処理が失われていることがわかります。
import Synchronization
// @unchecked Sendableで、ミュータブルなプロパティを許容
final class Test: @unchecked Sendable {
var num: Int = 0
// ランダムで数ナノ秒待機後にnumに1加算する
// @Sendableで並行安全性チェックを無効化している
@Sendable func add() async throws {
try await Task.sleep(nanoseconds: .random(in: 1...9))
num += 1
}
func run() async throws {
// 10000回、並行でadd()を呼び出す
try await withThrowingTaskGroup(of: Void.self) { group in
for _ in 1...10000 {
group.addTask {
try await self.add()
}
}
for try await _ in group {}
}
}
}
let sut = Test()
try await sut.run()
print(sut.num) // 9652
Mutexを使用してロックをかけてから書き込むと並行安全に
以下のコードのようにMutexを使うと、コンパイラを無理やり黙らせることなくSendableに準拠することができ、値も10000になり処理が失われていないことがわかります。
import Synchronization
final class Test: Sendable {
// macOS v15以降で使用可能なMutex
let num: Mutex<Int> = .init(0)
// ランダムで数ナノ秒待機後にnumに1加算する
func add() async throws {
try await Task.sleep(nanoseconds: .random(in: 1...9))
num.withLock { $0 += 1 }
}
func run() async throws {
// 10000回、並行でadd()を呼び出す
try await withThrowingTaskGroup(of: Void.self) { group in
for _ in 1...10000 {
group.addTask {
try await self.add()
}
}
for try await _ in group {}
}
}
}
let sut = Test()
try await sut.run()
print(sut.num.withLock { $0 })
Discussion