diff --git a/Sources/NIOConcurrencyHelpers/NIOLock.swift b/Sources/NIOConcurrencyHelpers/NIOLock.swift index e05c15bb6d..8b6e5cd6f8 100644 --- a/Sources/NIOConcurrencyHelpers/NIOLock.swift +++ b/Sources/NIOConcurrencyHelpers/NIOLock.swift @@ -14,200 +14,77 @@ #if canImport(Darwin) import Darwin -#elseif os(Windows) -import ucrt -import WinSDK -#elseif canImport(Glibc) -@preconcurrency import Glibc -#elseif canImport(Musl) -@preconcurrency import Musl -#elseif canImport(Bionic) -@preconcurrency import Bionic -#elseif canImport(WASILibc) -@preconcurrency import WASILibc -#if canImport(wasi_pthread) -import wasi_pthread -#endif #else -#error("The concurrency NIOLock module was unable to identify your C library.") +import Synchronization #endif -#if os(Windows) -@usableFromInline -typealias LockPrimitive = SRWLOCK -#else @usableFromInline -typealias LockPrimitive = pthread_mutex_t -#endif - -@usableFromInline -enum LockOperations: Sendable {} - -extension LockOperations { - @inlinable - static func create(_ mutex: UnsafeMutablePointer) { - mutex.assertValidAlignment() - - #if os(Windows) - InitializeSRWLock(mutex) - #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - var attr = pthread_mutexattr_t() - pthread_mutexattr_init(&attr) - debugOnly { - pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK)) - } - - let err = pthread_mutex_init(mutex, &attr) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - #endif - } - - @inlinable - static func destroy(_ mutex: UnsafeMutablePointer) { - mutex.assertValidAlignment() - - #if os(Windows) - // SRWLOCK does not need to be free'd - #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - let err = pthread_mutex_destroy(mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - #endif - } - - @inlinable - static func lock(_ mutex: UnsafeMutablePointer) { - mutex.assertValidAlignment() - - #if os(Windows) - AcquireSRWLockExclusive(mutex) - #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - let err = pthread_mutex_lock(mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - #endif - } - - @inlinable - static func unlock(_ mutex: UnsafeMutablePointer) { - mutex.assertValidAlignment() - - #if os(Windows) - ReleaseSRWLockExclusive(mutex) - #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - let err = pthread_mutex_unlock(mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") +final class LockStorage { + + #if canImport(Darwin) + @usableFromInline + @exclusivity(unchecked) + var mutex: os_unfair_lock_s + #else + @usableFromInline + let mutex: Mutex + #endif + + @usableFromInline + @exclusivity(unchecked) + var value: Value + + @inlinable + init(value: Value) { + #if canImport(Darwin) + self.mutex = os_unfair_lock_s() + #else + self.mutex = Mutex(()) #endif + self.value = value } -} - -// Tail allocate both the mutex and a generic value using ManagedBuffer. -// Both the header pointer and the elements pointer are stable for -// the class's entire lifetime. -// -// However, for safety reasons, we elect to place the lock in the "elements" -// section of the buffer instead of the head. The reasoning here is subtle, -// so buckle in. -// -// _As a practical matter_, the implementation of ManagedBuffer ensures that -// the pointer to the header is stable across the lifetime of the class, and so -// each time you call `withUnsafeMutablePointers` or `withUnsafeMutablePointerToHeader` -// the value of the header pointer will be the same. This is because ManagedBuffer uses -// `Builtin.addressOf` to load the value of the header, and that does ~magic~ to ensure -// that it does not invoke any weird Swift accessors that might copy the value. -// -// _However_, the header is also available via the `.header` field on the ManagedBuffer. -// This presents a problem! The reason there's an issue is that `Builtin.addressOf` and friends -// do not interact with Swift's exclusivity model. That is, the various `with` functions do not -// conceptually trigger a mutating access to `.header`. For elements this isn't a concern because -// there's literally no other way to perform the access, but for `.header` it's entirely possible -// to accidentally recursively read it. -// -// Our implementation is free from these issues, so we don't _really_ need to worry about it. -// However, out of an abundance of caution, we store the Value in the header, and the LockPrimitive -// in the trailing elements. We still don't use `.header`, but it's better to be safe than sorry, -// and future maintainers will be happier that we were cautious. -// -// See also: https://github.com/apple/swift/pull/40000 -@usableFromInline -final class LockStorage: ManagedBuffer { - - @inlinable - static func create(value: Value) -> Self { - let buffer = Self.create(minimumCapacity: 1) { _ in - value - } - // Intentionally using a force cast here to avoid a miss compiliation in 5.10. - // This is as fast as an unsafeDownCast since ManagedBuffer is inlined and the optimizer - // can eliminate the upcast/downcast pair - let storage = buffer as! Self - - storage.withUnsafeMutablePointers { _, lockPtr in - LockOperations.create(lockPtr) - } - - return storage - } - + @inlinable func lock() { - self.withUnsafeMutablePointerToElements { lockPtr in - LockOperations.lock(lockPtr) - } + #if canImport(Darwin) + let mutex_ptr = _getUnsafePointerToStoredProperties(self).assumingMemoryBound(to: os_unfair_lock_s.self) + os_unfair_lock_lock(mutex_ptr) + withExtendedLifetime(self) { } + #else + self.mutex._unsafeLock() + #endif } - + @inlinable func unlock() { - self.withUnsafeMutablePointerToElements { lockPtr in - LockOperations.unlock(lockPtr) - } - } - - @inlinable - deinit { - self.withUnsafeMutablePointerToElements { lockPtr in - LockOperations.destroy(lockPtr) - } - } - - @inlinable - func withLockPrimitive(_ body: (UnsafeMutablePointer) throws -> T) rethrows -> T { - try self.withUnsafeMutablePointerToElements { lockPtr in - try body(lockPtr) - } - } - - @inlinable - func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { - try self.withUnsafeMutablePointers { valuePtr, lockPtr in - LockOperations.lock(lockPtr) - defer { LockOperations.unlock(lockPtr) } - return try mutate(&valuePtr.pointee) - } + #if canImport(Darwin) + let mutex_ptr = _getUnsafePointerToStoredProperties(self).assumingMemoryBound(to: os_unfair_lock_s.self) + os_unfair_lock_unlock(mutex_ptr) + withExtendedLifetime(self) { } + #else + self.mutex._unsafeUnlock() + #endif } } -// This compiler guard is here becaue `ManagedBuffer` is already declaring -// Sendable unavailability after 6.1, which `LockStorage` inherits. -#if compiler(<6.2) @available(*, unavailable) extension LockStorage: Sendable {} -#endif -/// A threading lock based on `libpthread` instead of `libdispatch`. +/// A threading lock based on `Synchronization.Mutex` instead of `libdispatch`. /// /// - Note: ``NIOLock`` has reference semantics. /// -/// This object provides a lock on top of a single `pthread_mutex_t`. This kind +/// This object provides a lock on top of a single `Synchronization.Mutex`. This kind /// of lock is safe to use with `libpthread`-based threading models, such as the -/// one used by NIO. On Windows, the lock is based on the substantially similar -/// `SRWLOCK` type. +/// one used by NIO. public struct NIOLock { @usableFromInline - internal let _storage: LockStorage + let _storage: LockStorage /// Create a new lock. @inlinable public init() { - self._storage = .create(value: ()) + self._storage = LockStorage(value: ()) } /// Acquire the lock. @@ -227,11 +104,6 @@ public struct NIOLock { public func unlock() { self._storage.unlock() } - - @inlinable - internal func withLockPrimitive(_ body: (UnsafeMutablePointer) throws -> T) rethrows -> T { - try self._storage.withLockPrimitive(body) - } } extension NIOLock { @@ -259,10 +131,3 @@ extension NIOLock { } extension NIOLock: @unchecked Sendable {} - -extension UnsafeMutablePointer { - @inlinable - func assertValidAlignment() { - assert(UInt(bitPattern: self) % UInt(MemoryLayout.alignment) == 0) - } -} diff --git a/Sources/NIOConcurrencyHelpers/NIOLockedValueBox.swift b/Sources/NIOConcurrencyHelpers/NIOLockedValueBox.swift index 55260fb75d..83f30dcb81 100644 --- a/Sources/NIOConcurrencyHelpers/NIOLockedValueBox.swift +++ b/Sources/NIOConcurrencyHelpers/NIOLockedValueBox.swift @@ -17,25 +17,26 @@ /// - Note: ``NIOLockedValueBox`` has reference semantics and holds the `Value` /// alongside a lock behind a reference. /// -/// This is no different than creating a ``Lock`` and protecting all +/// This is no different than creating a ``NIOLock`` and protecting all /// accesses to a value using the lock. But it's easy to forget to actually /// acquire/release the lock in the correct place. ``NIOLockedValueBox`` makes /// that much easier. public struct NIOLockedValueBox { - @usableFromInline - internal let _storage: LockStorage + let _storage: LockStorage /// Initialize the `Value`. @inlinable public init(_ value: Value) { - self._storage = .create(value: value) + self._storage = LockStorage(value: value) } /// Access the `Value`, allowing mutation of it. @inlinable public func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { - try self._storage.withLockedValue(mutate) + self._storage.lock() + defer { self._storage.unlock() } + return try mutate(&self._storage.value) } /// Provides an unsafe view over the lock and its value. @@ -43,6 +44,7 @@ public struct NIOLockedValueBox { /// This can be beneficial when you require fine grained control over the lock in some /// situations but don't want lose the benefits of ``withLockedValue(_:)`` in others by /// switching to ``NIOLock``. + @inlinable public var unsafe: Unsafe { Unsafe(_storage: self._storage) } @@ -51,6 +53,11 @@ public struct NIOLockedValueBox { public struct Unsafe { @usableFromInline let _storage: LockStorage + + @inlinable + init(_storage: LockStorage) { + self._storage = _storage + } /// Manually acquire the lock. @inlinable @@ -72,9 +79,7 @@ public struct NIOLockedValueBox { public func withValueAssumingLockIsAcquired( _ mutate: (_ value: inout Value) throws -> Result ) rethrows -> Result { - try self._storage.withUnsafeMutablePointerToHeader { value in - try mutate(&value.pointee) - } + return try mutate(&self._storage.value) } } } diff --git a/Sources/NIOConcurrencyHelpers/lock.swift b/Sources/NIOConcurrencyHelpers/lock.swift index 572232a78b..7edbd90568 100644 --- a/Sources/NIOConcurrencyHelpers/lock.swift +++ b/Sources/NIOConcurrencyHelpers/lock.swift @@ -32,26 +32,24 @@ import wasi_pthread #error("The concurrency lock module was unable to identify your C library.") #endif -/// A threading lock based on `libpthread` instead of `libdispatch`. -/// -/// This object provides a lock on top of a single `pthread_mutex_t`. This kind -/// of lock is safe to use with `libpthread`-based threading models, such as the -/// one used by NIO. On Windows, the lock is based on the substantially similar -/// `SRWLOCK` type. -@available(*, deprecated, renamed: "NIOLock") -public final class Lock { - #if os(Windows) - fileprivate let mutex: UnsafeMutablePointer = - UnsafeMutablePointer.allocate(capacity: 1) - #else - fileprivate let mutex: UnsafeMutablePointer = - UnsafeMutablePointer.allocate(capacity: 1) - #endif +#if os(Windows) +@usableFromInline +typealias LockPrimitive = SRWLOCK +#else +@usableFromInline +typealias LockPrimitive = pthread_mutex_t +#endif + +@usableFromInline +enum LockOperations: Sendable {} + +extension LockOperations { + @inlinable + static func create(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() - /// Create a new lock. - public init() { #if os(Windows) - InitializeSRWLock(self.mutex) + InitializeSRWLock(mutex) #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) var attr = pthread_mutexattr_t() pthread_mutexattr_init(&attr) @@ -59,19 +57,73 @@ public final class Lock { pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK)) } - let err = pthread_mutex_init(self.mutex, &attr) + let err = pthread_mutex_init(mutex, &attr) precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") #endif } - deinit { + @inlinable + static func destroy(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + #if os(Windows) // SRWLOCK does not need to be free'd #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - let err = pthread_mutex_destroy(self.mutex) + let err = pthread_mutex_destroy(mutex) precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") #endif - mutex.deallocate() + } + + @inlinable + static func lock(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + + #if os(Windows) + AcquireSRWLockExclusive(mutex) + #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) + let err = pthread_mutex_lock(mutex) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + #endif + } + + @inlinable + static func unlock(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + + #if os(Windows) + ReleaseSRWLockExclusive(mutex) + #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) + let err = pthread_mutex_unlock(mutex) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + #endif + } +} + +extension UnsafeMutablePointer { + @inlinable + func assertValidAlignment() { + assert(UInt(bitPattern: self) % UInt(MemoryLayout.alignment) == 0) + } +} + +/// A threading lock based on `libpthread` instead of `libdispatch`. +/// +/// This object provides a lock on top of a single `pthread_mutex_t`. This kind +/// of lock is safe to use with `libpthread`-based threading models, such as the +/// one used by NIO. On Windows, the lock is based on the substantially similar +/// `SRWLOCK` type. +@available(*, deprecated, renamed: "NIOLock") +public final class Lock { + fileprivate let mutex: UnsafeMutablePointer = .allocate(capacity: 1) + + /// Create a new lock. + public init() { + LockOperations.create(self.mutex) + } + + deinit { + LockOperations.destroy(self.mutex) + self.mutex.deallocate() } /// Acquire the lock. @@ -79,12 +131,7 @@ public final class Lock { /// Whenever possible, consider using `withLock` instead of this method and /// `unlock`, to simplify lock handling. public func lock() { - #if os(Windows) - AcquireSRWLockExclusive(self.mutex) - #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - let err = pthread_mutex_lock(self.mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - #endif + LockOperations.lock(self.mutex) } /// Release the lock. @@ -92,12 +139,7 @@ public final class Lock { /// Whenever possible, consider using `withLock` instead of this method and /// `lock`, to simplify lock handling. public func unlock() { - #if os(Windows) - ReleaseSRWLockExclusive(self.mutex) - #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - let err = pthread_mutex_unlock(self.mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - #endif + LockOperations.unlock(self.mutex) } /// Acquire the lock for the duration of the given block. @@ -129,22 +171,31 @@ public final class Lock { /// This class provides a convenience addition to `Lock`: it provides the ability to wait /// until the state variable is set to a specific value to acquire the lock. public final class ConditionLock { - private var _value: T - private let mutex: NIOLock + @usableFromInline + @exclusivity(unchecked) + var _value: T + + @usableFromInline + let mutex: UnsafeMutablePointer = .allocate(capacity: 1) + #if os(Windows) - private let cond: UnsafeMutablePointer = + @usableFromInline + let cond: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: 1) #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - private let cond: UnsafeMutablePointer = + @usableFromInline + let cond: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: 1) #endif /// Create the lock, and initialize the state variable to `value`. /// /// - Parameter value: The initial value to give the state variable. + @inlinable public init(value: T) { self._value = value - self.mutex = NIOLock() + LockOperations.create(self.mutex) + #if os(Windows) InitializeConditionVariable(self.cond) #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) @@ -153,7 +204,11 @@ public final class ConditionLock { #endif } + @inlinable deinit { + LockOperations.destroy(self.mutex) + self.mutex.deallocate() + #if os(Windows) // condition variables do not need to be explicitly destroyed #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) @@ -164,13 +219,15 @@ public final class ConditionLock { } /// Acquire the lock, regardless of the value of the state variable. + @inlinable public func lock() { - self.mutex.lock() + LockOperations.lock(self.mutex) } /// Release the lock, regardless of the value of the state variable. + @inlinable public func unlock() { - self.mutex.unlock() + LockOperations.unlock(self.mutex) } /// The value of the state variable. @@ -178,6 +235,7 @@ public final class ConditionLock { /// Obtaining the value of the state variable requires acquiring the lock. /// This means that it is not safe to access this property while holding the /// lock: it is only safe to use it when not holding it. + @inlinable public var value: T { self.lock() defer { @@ -190,21 +248,21 @@ public final class ConditionLock { /// /// - Parameter wantedValue: The value to wait for the state variable /// to have before acquiring the lock. + @inlinable public func lock(whenValue wantedValue: T) { self.lock() while true { if self._value == wantedValue { break } - self.mutex.withLockPrimitive { mutex in - #if os(Windows) - let result = SleepConditionVariableSRW(self.cond, mutex, INFINITE, 0) - precondition(result, "\(#function) failed in SleepConditionVariableSRW with error \(GetLastError())") - #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) - let err = pthread_cond_wait(self.cond, mutex) - precondition(err == 0, "\(#function) failed in pthread_cond with error \(err)") - #endif - } + + #if os(Windows) + let result = SleepConditionVariableSRW(self.cond, self.mutex, INFINITE, 0) + precondition(result, "\(#function) failed in SleepConditionVariableSRW with error \(GetLastError())") + #elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded)) + let err = pthread_cond_wait(self.cond, self.mutex) + precondition(err == 0, "\(#function) failed in pthread_cond with error \(err)") + #endif } } @@ -216,6 +274,7 @@ public final class ConditionLock { /// - Parameter timeoutSeconds: The number of seconds to wait to acquire /// the lock before giving up. /// - Returns: `true` if the lock was acquired, `false` if the wait timed out. + @inlinable public func lock(whenValue wantedValue: T, timeoutSeconds: Double) -> Bool { precondition(timeoutSeconds >= 0) @@ -229,9 +288,8 @@ public final class ConditionLock { } let dwWaitStart = timeGetTime() - let result = self.mutex.withLockPrimitive { mutex in - SleepConditionVariableSRW(self.cond, mutex, dwMilliseconds, 0) - } + let result = SleepConditionVariableSRW(self.cond, self.mutex, dwMilliseconds, 0) + if !result { let dwError = GetLastError() if dwError == ERROR_TIMEOUT { @@ -261,20 +319,19 @@ public final class ConditionLock { ) assert(timeoutAbs.tv_nsec >= 0 && timeoutAbs.tv_nsec < Int(nsecPerSec)) assert(timeoutAbs.tv_sec >= curTime.tv_sec) - return self.mutex.withLockPrimitive { mutex -> Bool in - while true { - if self._value == wantedValue { - return true - } - switch pthread_cond_timedwait(self.cond, mutex, &timeoutAbs) { - case 0: - continue - case ETIMEDOUT: - self.unlock() - return false - case let e: - fatalError("caught error \(e) when calling pthread_cond_timedwait") - } + + while true { + if self._value == wantedValue { + return true + } + switch pthread_cond_timedwait(self.cond, self.mutex, &timeoutAbs) { + case 0: + continue + case ETIMEDOUT: + self.unlock() + return false + case let e: + fatalError("caught error \(e) when calling pthread_cond_timedwait") } } #else @@ -286,6 +343,7 @@ public final class ConditionLock { /// /// - Parameter newValue: The value to give to the state variable when we /// release the lock. + @inlinable public func unlock(withValue newValue: T) { self._value = newValue self.unlock()