Skip to content

Commit

Permalink
Initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
mattmassicotte committed Jul 27, 2024
1 parent 3b10051 commit 805d66d
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
28 changes: 28 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// swift-tools-version: 6.0

import PackageDescription

let package = Package(
name: "Lock",
platforms: [
.macOS(.v10_15),
.macCatalyst(.v13),
.iOS(.v13),
.tvOS(.v13),
.visionOS(.v1),
.watchOS(.v6),
],
products: [
.library(
name: "Lock",
targets: ["Lock"]),
],
targets: [
.target(
name: "Lock"),
.testTarget(
name: "LockTests",
dependencies: ["Lock"]),
],
swiftLanguageVersions: [.v6]
)
90 changes: 90 additions & 0 deletions Sources/Lock/AsyncLock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
public final class AsyncLock {
@TaskLocal private static var locked: Bool = false

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

case unlocked
case locked([Continuation])

mutating func addContinuation(_ continuation: Continuation) {
guard case var .locked(continuations) = self else {
fatalError("Continuations cannot be added when unlocked")
}

continuations.append(continuation)

self = .locked(continuations)
}

mutating func resumeNextContinuation() {
guard case var .locked(continuations) = self else {
fatalError("Continuations cannot be added when unlocked")
}

if continuations.isEmpty {
self = .unlocked
return
}

let continuation = continuations.removeFirst()

continuation.resume()

self = .locked(continuations)
}
}

private var state = State.unlocked

public init() {
}

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)
}
}
}

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

self.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
// }
// }
}

69 changes: 69 additions & 0 deletions Tests/LockTests/LockTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import Testing
import Lock

actor ReentrantActor {
var value = 42
let lock = AsyncLock()

// func doThing() async {
// 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
// }
// }

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

Check failure on line 19 in Tests/LockTests/LockTests.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

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()

Check failure on line 27 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 27 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

Check failure on line 27 in Tests/LockTests/LockTests.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
}
}

struct LockTests {
@Test
func lockUnlock() async {
let lock = AsyncLock()

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

@Test
func serializes() async {
let actor = ReentrantActor()
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 805d66d

Please sign in to comment.