Skip to content

Commit a68a8ae

Browse files
committed
Add Lock property wrapper
1 parent 16be24d commit a68a8ae

File tree

2 files changed

+61
-0
lines changed

2 files changed

+61
-0
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Foundation
2+
import os
3+
4+
/// A property wrapper that uses a lock to protect its value from concurrent reads/writes.
5+
///
6+
/// - warning: This is not appropriate for atomic operations. Use it for protecting class properties that might be accessed from multiple actors or queues.
7+
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
8+
@propertyWrapper public struct Lock<T: Sendable>: Sendable {
9+
10+
private let _value: OSAllocatedUnfairLock<T>
11+
12+
public var wrappedValue: T {
13+
get {
14+
_value.withLock { $0 }
15+
}
16+
nonmutating set {
17+
_value.withLock { $0 = newValue }
18+
}
19+
}
20+
21+
public init(wrappedValue: T) {
22+
self._value = OSAllocatedUnfairLock(initialState: wrappedValue)
23+
}
24+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import Foundation
2+
import Testing
3+
@testable import BuddyFoundation
4+
5+
@Suite("Lock Tests")
6+
struct LockTests {
7+
/// No expectations here other than it shouldn't crash.
8+
@Test func testConcurrentReadWriteProtected() async {
9+
let container = TestContainer()
10+
11+
let maxCount = 10000
12+
let taskCount = 4
13+
14+
await withTaskGroup { group in
15+
func runLoop() async {
16+
for i in 1...maxCount {
17+
container.value.insert(i)
18+
await Task.yield()
19+
container.value2.insert(i)
20+
}
21+
}
22+
23+
for _ in 0..<taskCount {
24+
group.addTask {
25+
await runLoop()
26+
}
27+
}
28+
29+
await group.waitForAll()
30+
}
31+
}
32+
}
33+
34+
private final class TestContainer: @unchecked Sendable {
35+
@Lock var value: Set<Int> = []
36+
@Lock var value2: Set<Int> = []
37+
}

0 commit comments

Comments
 (0)