🪢
[Swift] actor, Mutexを使ったデータ競合の排除
データ競合
class Counter {
var count: Int = 0
func increment() -> Int {
count += 1
return count
}
}
@Test
func classTest() {
let counter = Counter()
Task {
print(counter.increment())
}
Task {
print(counter.increment())
}
}
上記のCounterはマルチスレッドからcounter.increment()
にアクセスされるとデータ競合が起きてしまい、プリントされるパターンは保証されません。
プリントされるパターンは1, 2
, 2, 1
, 1, 1
, 2, 2
の4パターンでプリントされる可能性があります。
このデータ競合をactor
, Mutex
, OSAllocatedUnfairLock
, DispatchQueue
で排除することができ、1, 2
, 2, 1
のパターンのみにさせることができます。
名前 | 同期 | Platform |
---|---|---|
actor | 非同期処理 | iOS 15.0+, macOS 12.0+, Linux |
Mutex | 同期処理 | iOS 18.0+, macOS 15.0+, Linux |
OSAllocatedUnfairLock | 同期処理 | iOS 16.0+, macOS 13.0+ |
DispatchQueue.asyncAndWait | 同期処理 | iOS 12.0+, macOS 10.14+ |
DispatchQueue.sync | 同期処理 | iOS, macOS |
Counterを用いた各テストコード
1,000,000回程度で実行させるとデータ競合が確認できました。
◇ Test run started.
↳ Testing Library Version: 120
↳ Target Platform: arm64e-apple-macos14.0
◇ Test classTest() started.
DATA RACE DETECTED [2, 2]
DATA RACE DETECTED [2, 2]
DATA RACE DETECTED [1, 1]
DATA RACE DETECTED [2, 2]
- macOS 14.0
- Swift 6.1(Xcode 16.3 Beta)
- n = 1,000,000
テストケース | 平均実行時間 | 平均データ競合数 |
---|---|---|
class | 3.7s | 10 |
actor | 4.0s | 0 |
Mutex | 3.7s | 0 |
OSAllocatedUnfairLock | 3.85s | 0 |
DispatchQueue.asyncAndWait | 5.75s | 0 |
DispatchQueue.sync | 6000s | 0 |
final class ClassCounter: Counter, @unchecked Sendable {
var count: Int = 0
func increment() -> Int {
count += 1
return count
}
}
actor ActorCounter: Counter {
var count: Int = 0
func increment() -> Int {
count += 1
return count
}
}
import Synchronization
@available(macOS 15.0, *)
final class MutexCounter: Counter,Sendable {
let count: Mutex<Int> = .init(0)
func increment() -> Int {
count.withLock {
$0 += 1
return $0
}
}
}
import os.lock
final class OSLockCounter: Counter, Sendable {
let count = OSAllocatedUnfairLock(initialState: 0)
func increment() -> Int {
count.withLock {
$0 += 1
return $0
}
}
}
final class DispatchQueueCounter: Counter, @unchecked Sendable {
let queue = DispatchQueue(label: "Counter")
var value: Int = 0
func increment() -> Int {
return queue.asyncAndWait {
value += 1
return value
}
}
}
@Test
func classTest() async {
for _ in 0..<1_000_000 {
let counter = ClassCounter()
let values: [Int] = await countInMultiThreads(counter: counter)
if Set(values) != Set([1, 2]) {
print("DATA RACE DETECTED", values)
}
}
}
@Test
func actorTest() async {
for _ in 0..<1_000_000 {
let counter = ActorCounter()
let values: [Int] = await countInMultiThreads(counter: counter)
if Set(values) != Set([1, 2]) {
print("DATA RACE DETECTED", values)
}
}
}
@available(macOS 15.0, *)
@Test
func mutexTest() async {
for _ in 0..<1_000_000 {
let counter = MutexCounter()
let values: [Int] = await countInMultiThreads(counter: counter)
if Set(values) != Set([1, 2]) {
print("DATA RACE DETECTED", values)
}
}
}
@Test
func osLockTest() async {
for _ in 0..<1_000_000 {
let counter = OSLockCounter()
let values: [Int] = await countInMultiThreads(counter: counter)
if Set(values) != Set([1, 2]) {
print("DATA RACE DETECTED", values)
}
}
}
@Test
func dispatchQueueTest() async {
for _ in 0..<1_000_000_000 {
let counter = DispatchQueueCounter()
let values: [Int] = await countInMultiThreads(counter: counter)
if Set(values) != Set([1, 2]) {
print("DATA RACE DETECTED", values)
}
}
}
protocol Counter: Sendable {
func increment() async -> Int
}
func countInMultiThreads(counter: some Counter) async -> [Int] {
let values: [Int] = await withTaskGroup { group in
for _ in 0..<2 {
group.addTask {
await counter.increment()
}
}
var values: [Int] = []
for await value in group {
values.append(value)
}
return values
}
return values
}
Discussion