Swiftには@propertyWrapper
という便利な機能があり、プロパティの挙動をカプセル化できます。本記事では、スレッドセーフなプロパティラッパーAtomic
を実装し、さらにactor
との違いを比較してみます。
Atomic
の実装
以下のコードは、スレッドセーフなプロパティを実現するためにNSLock
を利用したAtomic
プロパティラッパーです。
import Foundation
@propertyWrapper
public struct Atomic<Value> {
private var value: Value
private let lock = NSLock()
public init(wrappedValue value: Value) {
self.value = value
}
public var wrappedValue: Value {
get { return load() }
set { store(newValue: newValue) }
}
func load() -> Value {
lock.lock()
defer { lock.unlock() }
return value
}
mutating func store(newValue: Value) {
lock.lock()
defer { lock.unlock() }
value = newValue
}
}
Atomic
の使い方
この Atomic
プロパティラッパーは、以下のように利用できます。
class Counter {
@Atomic var value: Int = 0
func increment() {
value += 1
}
}
let counter = Counter()
DispatchQueue.concurrentPerform(iterations: 1000) { _ in
counter.increment()
}
print(counter.value) // 1000 になるはず
actor
を使った場合の実装
Swift 5.5 以降では、actor
を使うことで、データ競合を防ぐことができます。
actor AtomicCounter {
private var value: Int = 0
func increment() {
value += 1
}
func getValue() -> Int {
return value
}
}
actor
の使い方
let counter = AtomicCounter()
Task {
await counter.increment()
print(await counter.getValue()) // 1
}
Atomic
(NSLock
) と actor
の違い
特性 |
Atomic (NSLock ) |
actor |
---|---|---|
スレッドセーフ |
NSLock を用いた明示的なロック制御 |
actor による自動的なデータ分離 |
排他制御 |
lock.lock() による同期処理 |
await による逐次処理 |
データ競合回避 | 明示的にロックを確保しながら値を読み書き |
actor 内部の状態はデフォルトでスレッドセーフ |
非同期対応 | 基本的に同期処理 (NSLock は非同期に適さない) |
async /await に対応し、スレッドをブロックしない |
パフォーマンス | ロックのオーバーヘッドがあるが、高速に動作 |
actor はタスクの順番待ちが発生するため、状況によっては遅くなることがある |
スレッド制約 |
NSLock は特定のスレッドでロック/アンロックを制御 |
actor のプロパティには他のスレッドから直接アクセスできない |
Atomic
vs actor
の使い分け
状況 |
Atomic (NSLock ) を使うべきケース |
actor を使うべきケース |
---|---|---|
同期処理 | シンプルなスレッドセーフな値の読み書きを行う | スレッド間で安全にデータを管理しつつ非同期処理を活用する |
パフォーマンス | ロックが短時間で済む場合 (Atomic のほうが高速) |
並行処理が多く、タスク順序管理が必要な場合 |
可読性 |
@propertyWrapper を利用し、通常のプロパティのように扱いたい |
await を活用しながら安全に非同期データ管理を行いたい |
非同期処理 |
Atomic は基本的に同期処理のため非同期には向かない |
actor は async/await で並行処理を管理できる |
まとめ
@propertyWrapper
を活用することで、スレッドセーフなプロパティを簡潔に管理できる Atomic
について書いてみました。一方で、Swift 5.5 以降の actor
を活用すれば、非同期環境でより安全にスレッド競合を防ぐこともできそうです。
-
単純なスレッドセーフな値の管理 →
Atomic
(NSLock
) -
非同期タスクと組み合わせる場合 →
actor