Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mutex back-port #35

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions [email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ let package = Package(
],
targets: [
.target(
name: "ConcurrencyExtras"
name: "ConcurrencyExtras",
swiftSettings: [
.enableExperimentalFeature("StaticExclusiveOnly"),
]
),
.testTarget(
name: "ConcurrencyExtrasTests",
Expand All @@ -27,7 +30,7 @@ let package = Package(
]
),
],
swiftLanguageVersions: [.v6]
swiftLanguageModes: [.v6]
)

#if !os(Windows)
Expand Down
166 changes: 99 additions & 67 deletions Sources/ConcurrencyExtras/LockIsolated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,79 +9,111 @@ public final class LockIsolated<Value>: @unchecked Sendable {
private var _value: Value
private let lock = NSRecursiveLock()

/// Initializes lock-isolated state around a value.
///
/// - Parameter value: A value to isolate with a lock.
public init(_ value: @autoclosure @Sendable () throws -> Value) rethrows {
self._value = try value()
}
#if compiler(>=6)
/// Initializes lock-isolated state around a value.
///
/// - Parameter value: A value to isolate with a lock.
public init(_ value: sending @autoclosure () throws -> sending Value) rethrows {
self._value = try value()
}
#else
public init(_ value: @autoclosure @Sendable () throws -> Value) rethrows {
self._value = try value()
}
#endif

public subscript<Subject: Sendable>(dynamicMember keyPath: KeyPath<Value, Subject>) -> Subject {
self.lock.sync {
self._value[keyPath: keyPath]
#if compiler(>=6)
public subscript<Subject>(dynamicMember keyPath: KeyPath<Value, Subject>) -> sending Subject {
self.lock.sync {
self._value[keyPath: keyPath]
}
}
}
#else
public subscript<Subject: Sendable>(dynamicMember keyPath: KeyPath<Value, Subject>) -> Subject {
self.lock.sync {
self._value[keyPath: keyPath]
}
}
#endif

/// Perform an operation with isolated access to the underlying value.
///
/// Useful for modifying a value in a single transaction.
///
/// ```swift
/// // Isolate an integer for concurrent read/write access:
/// var count = LockIsolated(0)
///
/// func increment() {
/// // Safely increment it:
/// self.count.withValue { $0 += 1 }
/// }
/// ```
///
/// - Parameter operation: An operation to be performed on the the underlying value with a lock.
/// - Returns: The result of the operation.
public func withValue<T: Sendable>(
_ operation: @Sendable (inout Value) throws -> T
) rethrows -> T {
try self.lock.sync {
var value = self._value
defer { self._value = value }
return try operation(&value)
#if compiler(>=6)
/// Perform an operation with isolated access to the underlying value.
///
/// Useful for modifying a value in a single transaction.
///
/// ```swift
/// // Isolate an integer for concurrent read/write access:
/// var count = LockIsolated(0)
///
/// func increment() {
/// // Safely increment it:
/// self.count.withValue { $0 += 1 }
/// }
/// ```
///
/// - Parameter operation: An operation to be performed on the the underlying value with a lock.
/// - Returns: The result of the operation.
public func withValue<T>(
_ operation: sending (inout sending Value) throws -> sending T
) rethrows -> T {
try self.lock.sync {
try operation(&_value)
}
}
}
#else
public func withValue<T: Sendable>(
_ operation: @Sendable (inout Value) throws -> T
) rethrows -> T {
try self.lock.sync {
var value = self._value
defer { self._value = value }
return try operation(&value)
}
}
#endif

/// Overwrite the isolated value with a new value.
///
/// ```swift
/// // Isolate an integer for concurrent read/write access:
/// var count = LockIsolated(0)
///
/// func reset() {
/// // Reset it:
/// self.count.setValue(0)
/// }
/// ```
///
/// > Tip: Use ``withValue(_:)`` instead of ``setValue(_:)`` if the value being set is derived
/// > from the current value. That is, do this:
/// >
/// > ```swift
/// > self.count.withValue { $0 += 1 }
/// > ```
/// >
/// > ...and not this:
/// >
/// > ```swift
/// > self.count.setValue(self.count + 1)
/// > ```
/// >
/// > ``withValue(_:)`` isolates the entire transaction and avoids data races between reading and
/// > writing the value.
///
/// - Parameter newValue: The value to replace the current isolated value with.
public func setValue(_ newValue: @autoclosure @Sendable () throws -> Value) rethrows {
try self.lock.sync {
self._value = try newValue()
#if compiler(>=6)
/// Overwrite the isolated value with a new value.
///
/// ```swift
/// // Isolate an integer for concurrent read/write access:
/// var count = LockIsolated(0)
///
/// func reset() {
/// // Reset it:
/// self.count.setValue(0)
/// }
/// ```
///
/// > Tip: Use ``withValue(_:)`` instead of ``setValue(_:)`` if the value being set is derived
/// > from the current value. That is, do this:
/// >
/// > ```swift
/// > self.count.withValue { $0 += 1 }
/// > ```
/// >
/// > ...and not this:
/// >
/// > ```swift
/// > self.count.setValue(self.count + 1)
/// > ```
/// >
/// > ``withValue(_:)`` isolates the entire transaction and avoids data races between reading and
/// > writing the value.
///
/// - Parameter newValue: The value to replace the current isolated value with.
public func setValue(_ newValue: sending @autoclosure () throws -> sending Value) rethrows {
try self.lock.sync {
self._value = try newValue()
}
}
}
#else
public func setValue(_ newValue: @autoclosure @Sendable () throws -> Value) rethrows {
try self.lock.sync {
self._value = try newValue()
}
}
#endif
}

extension LockIsolated where Value: Sendable {
Expand Down
67 changes: 67 additions & 0 deletions Sources/ConcurrencyExtras/Mutex.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#if compiler(>=6) && canImport(Darwin)
import Foundation

/// A synchronization primitive that protects shared mutable state via mutual exclusion.
///
/// A back-port of Swift's `Mutex` type for wider platform availability.
@_staticExclusiveOnly
@available(iOS, obsoleted: 18, message: "Use 'Synchronization.Mutex', instead.")
@available(macOS, obsoleted: 15, message: "Use 'Synchronization.Mutex', instead.")
@available(tvOS, obsoleted: 18, message: "Use 'Synchronization.Mutex', instead.")
@available(visionOS, obsoleted: 2, message: "Use 'Synchronization.Mutex', instead.")
@available(watchOS, obsoleted: 11, message: "Use 'Synchronization.Mutex', instead.")
public struct Mutex<Value: ~Copyable>: ~Copyable {
private let _lock = NSLock()
private let _box: Box

/// Initializes a value of this mutex with the given initial state.
///
/// - Parameter initialValue: The initial value to give to the mutex.
public init(_ initialValue: consuming sending Value) {
_box = Box(initialValue)
}

private final class Box {
var value: Value
init(_ initialValue: consuming sending Value) {
value = initialValue
}
}
}

extension Mutex: @unchecked Sendable where Value: ~Copyable {}

extension Mutex where Value: ~Copyable {
/// Calls the given closure after acquiring the lock and then releases ownership.
public borrowing func withLock<Result: ~Copyable, E: Error>(
_ body: (inout sending Value) throws(E) -> sending Result
) throws(E) -> sending Result {
_lock.lock()
defer { _lock.unlock() }
return try body(&_box.value)
}

/// Attempts to acquire the lock and then calls the given closure if successful.
public borrowing func withLockIfAvailable<Result: ~Copyable, E: Error>(
_ body: (inout sending Value) throws(E) -> sending Result
) throws(E) -> sending Result? {
guard _lock.try() else { return nil }
defer { _lock.unlock() }
return try body(&_box.value)
}
}

extension Mutex where Value == Void {
public borrowing func _unsafeLock() {
_lock.lock()
}

public borrowing func _unsafeTryLock() -> Bool {
_lock.try()
}

public borrowing func _unsafeUnlock() {
_lock.unlock()
}
}
#endif
Loading