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

Vendor in NIOs lock #1681

Merged
merged 2 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions NOTICES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ This product uses scripts derived from SwiftNIO's integration testing
framework: 'test_01_allocation_counts.sh', 'run-nio-alloc-counter-tests.sh' and
'test_functions.sh'.

If also uses derivations of SwiftNIO's lock 'NIOLock.swift' and locked value box
glbrntt marked this conversation as resolved.
Show resolved Hide resolved
'NIOLockedValueBox.swift'.

* LICENSE (Apache License 2.0):
* https://github.com/apple/swift-nio/blob/main/LICENSE.txt
* HOMEPAGE:
Expand Down
255 changes: 255 additions & 0 deletions Sources/GRPCCore/Internal/Concurrency Primities/Lock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
/*
* Copyright 2023, gRPC Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2022 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

#if canImport(Darwin)
import Darwin
#elseif canImport(Glibc)
import Glibc
#endif

@usableFromInline
typealias LockPrimitive = pthread_mutex_t

@usableFromInline
enum LockOperations {}

extension LockOperations {
@inlinable
static func create(_ mutex: UnsafeMutablePointer<LockPrimitive>) {
mutex.assertValidAlignment()

var attr = pthread_mutexattr_t()
pthread_mutexattr_init(&attr)

let err = pthread_mutex_init(mutex, &attr)
precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
}

@inlinable
static func destroy(_ mutex: UnsafeMutablePointer<LockPrimitive>) {
mutex.assertValidAlignment()

let err = pthread_mutex_destroy(mutex)
precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
}

@inlinable
static func lock(_ mutex: UnsafeMutablePointer<LockPrimitive>) {
mutex.assertValidAlignment()

let err = pthread_mutex_lock(mutex)
precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
}

@inlinable
static func unlock(_ mutex: UnsafeMutablePointer<LockPrimitive>) {
mutex.assertValidAlignment()

let err = pthread_mutex_unlock(mutex)
precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
}
}

// 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
return value
}
let storage = unsafeDowncast(buffer, to: Self.self)

storage.withUnsafeMutablePointers { _, lockPtr in
LockOperations.create(lockPtr)
}

return storage
}

@inlinable
func lock() {
self.withUnsafeMutablePointerToElements { lockPtr in
LockOperations.lock(lockPtr)
}
}

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

extension LockStorage: @unchecked Sendable {}

/// A threading lock based on `libpthread` instead of `libdispatch`.
///
/// - note: ``Lock`` has reference semantics.
///
/// 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.
@usableFromInline
struct Lock {
@usableFromInline
internal let _storage: LockStorage<Void>

/// Create a new lock.
@inlinable
init() {
self._storage = .create(value: ())
}

/// Acquire the lock.
///
/// Whenever possible, consider using `withLock` instead of this method and
/// `unlock`, to simplify lock handling.
@inlinable
func lock() {
self._storage.lock()
}

/// Release the lock.
///
/// Whenever possible, consider using `withLock` instead of this method and
/// `lock`, to simplify lock handling.
@inlinable
func unlock() {
self._storage.unlock()
}

@inlinable
internal func withLockPrimitive<T>(
_ body: (UnsafeMutablePointer<LockPrimitive>) throws -> T
) rethrows -> T {
return try self._storage.withLockPrimitive(body)
}
}

extension Lock {
/// Acquire the lock for the duration of the given block.
///
/// This convenience method should be preferred to `lock` and `unlock` in
/// most situations, as it ensures that the lock will be released regardless
/// of how `body` exits.
///
/// - Parameter body: The block to execute while holding the lock.
/// - Returns: The value returned by the block.
@inlinable
func withLock<T>(_ body: () throws -> T) rethrows -> T {
self.lock()
defer {
self.unlock()
}
return try body()
}
}

extension Lock: Sendable {}

extension UnsafeMutablePointer {
@inlinable
func assertValidAlignment() {
assert(UInt(bitPattern: self) % UInt(MemoryLayout<Pointee>.alignment) == 0)
}
}

@usableFromInline
struct LockedValueBox<Value> {
@usableFromInline
let storage: LockStorage<Value>

@inlinable
init(_ value: Value) {
self.storage = .create(value: value)
}

@inlinable
func withLockedValue<T>(_ mutate: (inout Value) throws -> T) rethrows -> T {
return try self.storage.withLockedValue(mutate)
}
}

extension LockedValueBox: Sendable where Value: Sendable {}