🪢
[Swift] actor, Mutexを使ったデータ競合の排除
データ競合
class ClassCounter {
var count: Int = 0
func increment() -> Int {
count += 1
return count
}
}
@Test
func classTest() {
let counter = ClassCounter()
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 |
protocol Counter: Sendable {
func increment() async -> Int
}
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)
}
}
}
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
DispatchQueueCounterだけCounterをちゃんと継承できていないように見えるんですがどうでしょうか?改めて読んでみると自分でも紛らわしかったのですが、一番上にCounter(class)を書いていて、実際には
protocol Counter: Sendableを下に書いていて、そのprotocolに準拠する形になっていました。以下のように修正しました。
下のコードブロックの中で下の方にprotocolの宣言があったので、上に持ってきました。
指摘部分と違っていたら教えてください。