-
Notifications
You must be signed in to change notification settings - Fork 712
Update our locks to use Synchronization.Mutex or os_unfair_lock #3428
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
Open
asdf-bro
wants to merge
4
commits into
apple:main
Choose a base branch
from
asdf-bro:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<LockPrimitive>) { | ||
| 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<LockPrimitive>) { | ||
| 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<LockPrimitive>) { | ||
| 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<LockPrimitive>) { | ||
| 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<Value> { | ||
|
|
||
| #if canImport(Darwin) | ||
| @usableFromInline | ||
| @exclusivity(unchecked) | ||
| var mutex: os_unfair_lock_s | ||
| #else | ||
| @usableFromInline | ||
| let mutex: Mutex<Void> | ||
| #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<Value>: ManagedBuffer<Value, LockPrimitive> { | ||
|
|
||
| @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) | ||
| _fixLifetime(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<T>(_ body: (UnsafeMutablePointer<LockPrimitive>) throws -> T) rethrows -> T { | ||
| try self.withUnsafeMutablePointerToElements { lockPtr in | ||
| try body(lockPtr) | ||
| } | ||
| } | ||
|
|
||
| @inlinable | ||
| func withLockedValue<T>(_ 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) | ||
| _fixLifetime(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<Void> | ||
| let _storage: LockStorage<Void> | ||
|
|
||
| /// 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<T>(_ body: (UnsafeMutablePointer<LockPrimitive>) 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<Pointee>.alignment) == 0) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the
Mutexcase, let's put the storage into theMutexas well.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we do that, we can't implement
NIOLockedValueBox.Unsafe; the only public APIs onMutex<non Void>are the twowithLockmethods.We could use
Synchronization._MutexHandleinstead ofMutex<Void>, if you prefer._MutexHandleis public and has public locking methods.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh that's unfortunate. I'd like us to avoid using underscored parts of Swift to the greatest extent possible.