diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index 67c847f..1f03ed1 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -18,7 +18,10 @@ let package = Package( ], targets: [ .target( - name: "ConcurrencyExtras" + name: "ConcurrencyExtras", + swiftSettings: [ + .enableExperimentalFeature("StaticExclusiveOnly"), + ] ), .testTarget( name: "ConcurrencyExtrasTests", @@ -27,7 +30,7 @@ let package = Package( ] ), ], - swiftLanguageVersions: [.v6] + swiftLanguageModes: [.v6] ) #if !os(Windows) diff --git a/Sources/ConcurrencyExtras/LockIsolated.swift b/Sources/ConcurrencyExtras/LockIsolated.swift index 640d88c..c0b8db1 100644 --- a/Sources/ConcurrencyExtras/LockIsolated.swift +++ b/Sources/ConcurrencyExtras/LockIsolated.swift @@ -9,79 +9,111 @@ public final class LockIsolated: @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(dynamicMember keyPath: KeyPath) -> Subject { - self.lock.sync { - self._value[keyPath: keyPath] + #if compiler(>=6) + public subscript(dynamicMember keyPath: KeyPath) -> sending Subject { + self.lock.sync { + self._value[keyPath: keyPath] + } } - } + #else + public subscript(dynamicMember keyPath: KeyPath) -> 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( - _ 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( + _ operation: sending (inout sending Value) throws -> sending T + ) rethrows -> T { + try self.lock.sync { + try operation(&_value) + } } - } + #else + public func withValue( + _ 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 { diff --git a/Sources/ConcurrencyExtras/Mutex.swift b/Sources/ConcurrencyExtras/Mutex.swift new file mode 100644 index 0000000..a6783e8 --- /dev/null +++ b/Sources/ConcurrencyExtras/Mutex.swift @@ -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: ~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( + _ 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( + _ 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