Skip to content

Commit

Permalink
Separating out recursive lock
Browse files Browse the repository at this point in the history
  • Loading branch information
mattmassicotte committed Jul 27, 2024
1 parent 007bd1d commit a0b53e3
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 67 deletions.
38 changes: 22 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
# Lock
A lock for Swift concurrency

This package exposes a single type: `AsyncLock`. This type allows you to define **asynchronous** critical sections. One only task can enter a critical section at a time. The lock can be used recursively, but requires a little care to do so. Unfortunately, the method that implements the recursive functionality currently [crashes the compiler](https://github.com/swiftlang/swift/issues/75523).
This package exposes two singles types: `AsyncLock` and `AsyncRecursiveLock`. These allow you to define **asynchronous** critical sections. One only task can enter a critical section at a time.

Unfortunately, the method that implements the recursive functionality currently [crashes the compiler](https://github.com/swiftlang/swift/issues/75523).

This is a handy tool for dealing with actor reentrancy.

Expand All @@ -31,32 +33,36 @@ dependencies: [

The `AsyncLock` type is **non-Sendable**. This is an intentional choice to disallow sharing the lock across isolation domains. If you want to something like that, first think really hard about why and then check out [Semaphore][].

Note that trying to acquire an already-locked `AsyncLock` **will** deadlock your actor.

```swift
actor MyActor {
var value = 42
let lock = AsyncLock()
var value = 42
let lock = AsyncLock()
let recursiveLock = AsyncRecursiveLock()

func hasCriticalSections() async {
await lock.lock()
func hasCriticalSections() async {
// no matter how many tasks call this method,
// only one will be able to execute at a time
await lock.lock()

self.value = await otherObject.getValue()
self.value = await otherObject.getValue()

await lock.unlock()
}

// This version enables recursive locking, but currently crashes the compiler
func hasCriticalSectionsBlock() async {
await lock.withLock {
lock.unlock()
}

// currently unavailable due to a compiler bug
func hasCriticalSectionsBlock() async {
await recursiveLock.withLock {
// acquiring this multiple times within the same task is safe
await recursiveLock.withLock {
self.value = await otherObject.getValue()

}
}
}
}
}
```

`AsyncLock` can be acquired recursively **exclusively** with `withLock` blocks. If you try to re-lock without being nested within a `withLock`, you **will** deadlock your actor.

## Contributing and Collaboration

I would love to hear from you! Issues or pull requests work great. Both a [Matrix space][matrix] and [Discord][discord] are also available for live help, but I have a strong bias towards answering in the form of documentation.
Expand Down
55 changes: 19 additions & 36 deletions Sources/Lock/AsyncLock.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
public final class AsyncLock {
@TaskLocal private static var locked: Bool = false

private enum State {
typealias Continuation = CheckedContinuation<Void, Never>

Expand Down Expand Up @@ -41,50 +39,35 @@ public final class AsyncLock {
}

public func lock(isolation: isolated (any Actor)? = #isolation) async {
if Self.locked == true {
return
}

switch state {
case .unlocked:
self.state = .locked([])
case .locked:
await withCheckedContinuation { continuation in
self.state.addContinuation(continuation)
state.addContinuation(continuation)
}
}
}

public func unlock(isolation: isolated (any Actor)? = #isolation) async {
if Self.locked == true {
return
}

self.state.resumeNextContinuation()
public func unlock() {
state.resumeNextContinuation()
}

// this currently crashes the compiler
// public func withLock<T>(
// isolation: isolated (any Actor)? = #isolation,
// @_inheritActorContext _ block: @isolated(any) @escaping () async throws -> sending T
// ) async rethrows -> sending T {
// if Self.locked == true {
// return try await block()
// }
//
// return await lock()
//
// do {
// let value = try await Self.$locked.withValue(true) {
// try await block()
// }
//
// await unlock()
//
// return value
// } catch {
// throw error
// }
// }
// public func withLock<T>(
// isolation: isolated (any Actor)? = #isolation,
// _ block: @isolated(any) () async throws -> sending T
// ) async rethrows -> sending T {
// do {
// let value = try await block()
//
// unlock()
//
// return value
// } catch {
// unlock()
//
// throw error
// }
// }
}

49 changes: 49 additions & 0 deletions Sources/Lock/AsyncRecursiveLock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
public final class AsyncRecursiveLock {
@TaskLocal private static var lockCount = 0

private let internalLock = AsyncLock()

public init() {
}

public func lock(isolation: isolated (any Actor)? = #isolation) async {
// precondition(lockCount >= 0)
//
// lockCount += 1
//
// if lockCount == 1 {
// await internalLock.lock()
// }
}
//
public func unlock() {
// lockCount -= 1
//
// precondition(lockCount >= 0)
//
// if lockCount == 0 {
// internalLock.unlock()
// }
}
//
// public func withLock<T>(
// isolation: isolated (any Actor)? = #isolation,
// _ block: () async throws -> sending T
// ) async rethrows -> sending T {
// await lock()
// // bug
//// defer { unlock() }
//
// do {
// let value = try await block()
//
// unlock()
//
// return value
// } catch {
// unlock()
//
// throw error
// }
// }
}
17 changes: 2 additions & 15 deletions Tests/LockTests/LockTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@ actor ReentrantActor {

func doThing() async {
await lock.lock()

Check failure on line 19 in Tests/LockTests/LockTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=macOS)

pattern that the region based isolation checker does not understand how to check. Please file a bug

Check failure on line 19 in Tests/LockTests/LockTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=tvOS Simulator,name=Apple TV)

pattern that the region based isolation checker does not understand how to check. Please file a bug
defer { lock.unlock() }

try! #require(self.value == 42)
self.value = 0
try! await Task.sleep(nanoseconds: 1_000_000)
try! #require(self.value == 0)
self.value = 42

await lock.unlock()
}
}

Expand All @@ -34,7 +33,7 @@ struct LockTests {
let lock = AsyncLock()

await lock.lock()
await lock.unlock()
lock.unlock()
}

@Test
Expand All @@ -54,16 +53,4 @@ struct LockTests {
await task.value
}
}

// @Test
// func recursion() async {
// let lock = AsyncLock()
//
// await lock.withLock {
// await lock.lock()
// await lock.withLock {
// }
// await lock.unlock()
// }
// }
}
58 changes: 58 additions & 0 deletions Tests/LockTests/RecursiveLockTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import Testing
import Lock

actor RecursiveReentrantActor {
var value = 42
let lock = AsyncRecursiveLock()

func doThing() async {
await lock.lock()

Check failure on line 9 in Tests/LockTests/RecursiveLockTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=macOS,variant=Mac Catalyst)

pattern that the region based isolation checker does not understand how to check. Please file a bug

Check failure on line 9 in Tests/LockTests/RecursiveLockTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=tvOS Simulator,name=Apple TV)

pattern that the region based isolation checker does not understand how to check. Please file a bug

Check failure on line 9 in Tests/LockTests/RecursiveLockTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=visionOS Simulator,name=Apple Vision Pro)

pattern that the region based isolation checker does not understand how to check. Please file a bug
defer { lock.unlock() }

try! #require(self.value == 42)
self.value = 0
try! await Task.sleep(nanoseconds: 1_000_000)
try! #require(self.value == 0)
self.value = 42
}
}

struct RecursiveLockTests {
@Test
func lockUnlock() async {
let lock = AsyncRecursiveLock()

await lock.lock()
lock.unlock()
}

// @Test
// func serializes() async {
// let actor = RecursiveReentrantActor()
// var tasks = [Task<Void, Never>]()
//
// for _ in 0..<1000 {
// let task = Task {
// await actor.doThing()
// }
//
// tasks.append(task)
// }
//
// for task in tasks {
// await task.value
// }
// }

// @Test
// func recursion() async {
// let lock = AsyncLock()
//
// await lock.withLock {
// await lock.lock()
// await lock.withLock {
// }
// await lock.unlock()
// }
// }
}

0 comments on commit a0b53e3

Please sign in to comment.