Skip to content

Commit d18d118

Browse files
authored
ContainerizationExtras: Add AsyncMutex (#324)
We already have `AsyncLock`, but in some cases it'd be nice to have the type protect a piece of data that you access through the lock, much like the Synchronization frameworks new `Mutex` type. This change adds such a type.
1 parent 2443a24 commit d18d118

File tree

2 files changed

+191
-0
lines changed

2 files changed

+191
-0
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2025 Apple Inc. and the Containerization project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
/// `AsyncMutex` provides a mutex that protects a piece of data, with the main benefit being that it
18+
/// is safe to call async methods while holding the lock. This is primarily used in spots
19+
/// where an actor makes sense, but we may need to ensure we don't fall victim to actor
20+
/// reentrancy issues.
21+
public actor AsyncMutex<T: Sendable> {
22+
private final class Box: @unchecked Sendable {
23+
var value: T
24+
init(_ value: T) {
25+
self.value = value
26+
}
27+
}
28+
29+
private var busy = false
30+
private var queue: ArraySlice<CheckedContinuation<(), Never>> = []
31+
private let box: Box
32+
33+
public init(_ initialValue: T) {
34+
self.box = Box(initialValue)
35+
}
36+
37+
/// withLock provides a scoped locking API to run a function while holding the lock.
38+
/// The protected value is passed to the closure for safe access.
39+
public func withLock<R: Sendable>(_ body: @Sendable @escaping (inout T) async throws -> R) async rethrows -> R {
40+
while self.busy {
41+
await withCheckedContinuation { cc in
42+
self.queue.append(cc)
43+
}
44+
}
45+
46+
self.busy = true
47+
48+
defer {
49+
self.busy = false
50+
if let next = self.queue.popFirst() {
51+
next.resume(returning: ())
52+
} else {
53+
self.queue = []
54+
}
55+
}
56+
57+
return try await body(&self.box.value)
58+
}
59+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2025 Apple Inc. and the Containerization project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import Foundation
18+
import Testing
19+
20+
@testable import ContainerizationExtras
21+
22+
final class AsyncMutexTests {
23+
@Test
24+
func testBasicModification() async throws {
25+
let mutex = AsyncMutex(0)
26+
27+
let result = await mutex.withLock { value in
28+
value += 1
29+
return value
30+
}
31+
32+
#expect(result == 1)
33+
}
34+
35+
@Test
36+
func testMultipleModifications() async throws {
37+
let mutex = AsyncMutex(0)
38+
39+
await mutex.withLock { value in
40+
value += 5
41+
}
42+
43+
let result = await mutex.withLock { value in
44+
value += 10
45+
return value
46+
}
47+
48+
#expect(result == 15)
49+
}
50+
51+
@Test
52+
func testConcurrentAccess() async throws {
53+
let mutex = AsyncMutex(0)
54+
let iterations = 100
55+
56+
await withTaskGroup(of: Void.self) { group in
57+
for _ in 0..<iterations {
58+
group.addTask {
59+
await mutex.withLock { value in
60+
value += 1
61+
}
62+
}
63+
}
64+
}
65+
66+
let finalValue = await mutex.withLock { value in
67+
value
68+
}
69+
70+
#expect(finalValue == iterations)
71+
}
72+
73+
@Test
74+
func testAsyncOperationsUnderLock() async throws {
75+
let mutex = AsyncMutex([Int]())
76+
77+
await mutex.withLock { value in
78+
try? await Task.sleep(for: .milliseconds(10))
79+
value.append(1)
80+
}
81+
82+
await mutex.withLock { value in
83+
try? await Task.sleep(for: .milliseconds(10))
84+
value.append(2)
85+
}
86+
87+
let result = await mutex.withLock { value in
88+
value
89+
}
90+
91+
#expect(result == [1, 2])
92+
}
93+
94+
@Test
95+
func testThrowingClosure() async throws {
96+
let mutex = AsyncMutex(0)
97+
98+
await #expect(throws: POSIXError.self) {
99+
try await mutex.withLock { value in
100+
value += 1
101+
throw POSIXError(.ENOENT)
102+
}
103+
}
104+
105+
// Value should still be modified even though closure threw
106+
let result = await mutex.withLock { value in
107+
value
108+
}
109+
110+
#expect(result == 1)
111+
}
112+
113+
@Test
114+
func testComplexDataStructure() async throws {
115+
struct Counter: Sendable {
116+
var count: Int
117+
var label: String
118+
}
119+
120+
let mutex = AsyncMutex(Counter(count: 0, label: "test"))
121+
122+
await mutex.withLock { value in
123+
value.count += 10
124+
value.label = "modified"
125+
}
126+
127+
await mutex.withLock { value in
128+
#expect(value.count == 10)
129+
#expect(value.label == "modified")
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)