Skip to content

Commit

Permalink
More tests, update CI
Browse files Browse the repository at this point in the history
  • Loading branch information
mattmassicotte committed Aug 20, 2024
1 parent a045e59 commit f94a7c3
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 98 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
name: Test
runs-on: macOS-14
env:
DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer
DEVELOPER_DIR: /Applications/Xcode_16_beta_5.app/Contents/Developer
strategy:
matrix:
destination:
Expand Down
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,10 @@ A lock for Swift concurrency

This package exposes two types: `AsyncLock` and `AsyncRecursiveLock`. These allow you to define **asynchronous** critical sections. One only task can enter a critical section at a time. Unlike a traditional lock, you can safely make async calls while these locks are held.

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.

> [!CAUTION]
> This doesn't yet have great test coverage and it cannot be built with the compiler available in Xcode 16b4.
> This doesn't yet have great test coverage.
Some other concurrency packages you might find useful are [Queue](https://github.com/mattmassicotte/Queue) and [Semaphore][].

Expand Down
32 changes: 16 additions & 16 deletions Sources/Lock/AsyncLock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,21 @@ public final class AsyncLock {
state.resumeNextContinuation()
}

// 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
// }
// }
public func withLock<T: Sendable>(
isolation: isolated (any Actor)? = #isolation,
_ block: @isolated(any) () async throws -> T
) async rethrows -> T {
do {
let value = try await block()

unlock()

return value
} catch {
unlock()

throw error
}
}
}

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

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: Sendable>(
isolation: isolated (any Actor)? = #isolation,
_ block: () async throws -> T
) async rethrows -> T {
if Self.locked {
return try await block()
}

await internalLock.lock()

do {
let value = try await Self.$locked.withValue(true) {
try await block()
}

internalLock.unlock()

return value
} catch {
internalLock.unlock()

throw error
}
}
//
// 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
// }
// }
}
73 changes: 35 additions & 38 deletions Tests/LockTests/RecursiveLockTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,50 @@ actor RecursiveReentrantActor {
let lock = AsyncRecursiveLock()

func doThing() async {
await lock.lock()
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.withLock {
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 {
func recursion() async {
let lock = AsyncRecursiveLock()

await lock.withLock {
await lock.withLock {
}
}
}

@Test
func lockAndUnlock() async {
let lock = AsyncLock()

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()
// }
// }
@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
}
}
}

0 comments on commit f94a7c3

Please sign in to comment.