Skip to content

Commit a9dd8d8

Browse files
committed
Add manual control to NIOLockedValueBox
Motivation: NIOLockedValueBox has a 'safer' API than NIOLock as it only provides scoped access to its boxed value. NIOLock requires users to only access protected state while the lock is acquired. As such NIOLockedValueBox should be preferred where possible. However, there are cases where manual control must be used (such as storing a continuation) and users must use a NIOLock for this. There are two downsides to this: 1. All other access to the protected state must use the NIOLock API putting the onus on the developer to only access the protected state while the lock is held. 2. NIOLock can't store its protected state inline which typically results in users storing it on a class. Modifications: - Add an 'unsafe' view to NIOLockedValueBox which allows users to manually control the lock and access its protected state - Update NIOAsyncWriter and NIOThrowingAsyncSequenceProducer to use NIOLockedValueBox Result: - Safer locking API is used in more places - Fewer allocations
1 parent 0f4c110 commit a9dd8d8

11 files changed

+225
-130
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"mallocCountTotal" : 10
2+
"mallocCountTotal" : 8
33
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"mallocCountTotal" : 10
2+
"mallocCountTotal" : 8
33
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"mallocCountTotal" : 10
2+
"mallocCountTotal" : 8
33
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"mallocCountTotal" : 10
2+
"mallocCountTotal" : 8
33
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"mallocCountTotal" : 10
2+
"mallocCountTotal" : 8
33
}

Sources/NIOConcurrencyHelpers/NIOLockedValueBox.swift

+37
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,43 @@ public struct NIOLockedValueBox<Value> {
3737
public func withLockedValue<T>(_ mutate: (inout Value) throws -> T) rethrows -> T {
3838
return try self._storage.withLockedValue(mutate)
3939
}
40+
41+
/// Provides an unsafe view over the lock and its value.
42+
///
43+
/// This can be beneficial when you require fine grained control over the lock in some
44+
/// situations but don't want lose the benefits of ``withLockedValue(_:)`` in others by
45+
/// switching to ``NIOLock``.
46+
public var unsafe: Unsafe {
47+
Unsafe(_storage: self._storage)
48+
}
49+
50+
/// Provides an unsafe view over the lock and its value.
51+
public struct Unsafe {
52+
@usableFromInline
53+
let _storage: LockStorage<Value>
54+
55+
/// Manually acquire the lock.
56+
@inlinable
57+
public func lock() {
58+
self._storage.lock()
59+
}
60+
61+
/// Manually release the lock.
62+
@inlinable
63+
public func unlock() {
64+
self._storage.unlock()
65+
}
66+
67+
/// Mutate the value, assuming the lock has been acquired manually.
68+
@inlinable
69+
public func withValueAssumingLockIsAcquired<T>(
70+
_ mutate: (inout Value) throws -> T
71+
) rethrows -> T {
72+
return try self._storage.withUnsafeMutablePointerToHeader { value in
73+
try mutate(&value.pointee)
74+
}
75+
}
76+
}
4077
}
4178

4279
extension NIOLockedValueBox: Sendable where Value: Sendable {}

Sources/NIOCore/AsyncSequences/NIOAsyncWriter.swift

+68-49
Original file line numberDiff line numberDiff line change
@@ -414,12 +414,12 @@ extension NIOAsyncWriter {
414414
extension NIOAsyncWriter {
415415
/// This is the underlying storage of the writer. The goal of this is to synchronize the access to all state.
416416
@usableFromInline
417-
/* fileprivate */ internal final class Storage: @unchecked Sendable {
417+
/* fileprivate */ internal struct Storage: Sendable {
418418
/// Internal type to generate unique yield IDs.
419419
///
420420
/// This type has reference semantics.
421421
@usableFromInline
422-
struct YieldIDGenerator {
422+
struct YieldIDGenerator: Sendable {
423423
/// A struct representing a unique yield ID.
424424
@usableFromInline
425425
struct YieldID: Equatable, Sendable {
@@ -445,47 +445,61 @@ extension NIOAsyncWriter {
445445
}
446446
}
447447

448-
/// The lock that protects our state.
449-
@usableFromInline
450-
/* private */ internal let _lock = NIOLock()
451448
/// The counter used to assign an ID to all our yields.
452449
@usableFromInline
453450
/* private */ internal let _yieldIDGenerator = YieldIDGenerator()
454451
/// The state machine.
455452
@usableFromInline
456-
/* private */ internal var _stateMachine: StateMachine
453+
/* private */ internal let _state: NIOLockedValueBox<State>
454+
455+
@usableFromInline
456+
struct State: Sendable {
457+
@usableFromInline
458+
var stateMachine: StateMachine
459+
@usableFromInline
460+
var didSuspend: (@Sendable () -> Void)?
461+
462+
@inlinable
463+
init(stateMachine: StateMachine) {
464+
self.stateMachine = stateMachine
465+
self.didSuspend = nil
466+
}
467+
}
468+
457469
/// Hook used in testing.
458470
@usableFromInline
459-
internal var _didSuspend: (() -> Void)?
471+
internal func _setDidSuspend(_ didSuspend: (@Sendable () -> Void)?) {
472+
self._state.withLockedValue {
473+
$0.didSuspend = didSuspend
474+
}
475+
}
460476

461477
@inlinable
462478
internal var isWriterFinished: Bool {
463-
self._lock.withLock { self._stateMachine.isWriterFinished }
479+
self._state.withLockedValue { $0.stateMachine.isWriterFinished }
464480
}
465481

466482
@inlinable
467483
internal var isSinkFinished: Bool {
468-
self._lock.withLock { self._stateMachine.isSinkFinished }
484+
self._state.withLockedValue { $0.stateMachine.isSinkFinished }
469485
}
470486

471487
@inlinable
472488
/* fileprivate */ internal init(
473489
isWritable: Bool,
474490
delegate: Delegate
475491
) {
476-
self._stateMachine = .init(
477-
isWritable: isWritable,
478-
delegate: delegate
479-
)
492+
let state = State(stateMachine: StateMachine(isWritable: isWritable, delegate: delegate))
493+
self._state = NIOLockedValueBox(state)
480494
}
481495

482496
@inlinable
483497
/* fileprivate */ internal func setWritability(to writability: Bool) {
484498
// We must not resume the continuation while holding the lock
485499
// because it can deadlock in combination with the underlying ulock
486500
// in cases where we race with a cancellation handler
487-
let action = self._lock.withLock {
488-
self._stateMachine.setWritability(to: writability)
501+
let action = self._state.withLockedValue {
502+
$0.stateMachine.setWritability(to: writability)
489503
}
490504

491505
switch action {
@@ -516,39 +530,42 @@ extension NIOAsyncWriter {
516530

517531
return try await withTaskCancellationHandler {
518532
// We are manually locking here to hold the lock across the withCheckedContinuation call
519-
self._lock.lock()
533+
let unsafe = self._state.unsafe
534+
unsafe.lock()
520535

521-
let action = self._stateMachine.yield(yieldID: yieldID)
536+
let action = unsafe.withValueAssumingLockIsAcquired {
537+
$0.stateMachine.yield(yieldID: yieldID)
538+
}
522539

523540
switch action {
524541
case .callDidYield(let delegate):
525542
// We are allocating a new Deque for every write here
526-
self._lock.unlock()
543+
unsafe.unlock()
527544
delegate.didYield(contentsOf: Deque(sequence))
528545
self.unbufferQueuedEvents()
529546
return .yielded
530547

531548
case .throwError(let error):
532-
self._lock.unlock()
549+
unsafe.unlock()
533550
throw error
534551

535552
case .suspendTask:
536553
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<StateMachine.YieldResult, Error>) in
537-
self._stateMachine.yield(
538-
continuation: continuation,
539-
yieldID: yieldID
540-
)
554+
let didSuspend = unsafe.withValueAssumingLockIsAcquired {
555+
$0.stateMachine.yield(continuation: continuation, yieldID: yieldID)
556+
return $0.didSuspend
557+
}
541558

542-
self._lock.unlock()
543-
self._didSuspend?()
559+
unsafe.unlock()
560+
didSuspend?()
544561
}
545562
}
546563
} onCancel: {
547564
// We must not resume the continuation while holding the lock
548565
// because it can deadlock in combination with the underlying ulock
549566
// in cases where we race with a cancellation handler
550-
let action = self._lock.withLock {
551-
self._stateMachine.cancel(yieldID: yieldID)
567+
let action = self._state.withLockedValue {
568+
$0.stateMachine.cancel(yieldID: yieldID)
552569
}
553570

554571
switch action {
@@ -580,39 +597,41 @@ extension NIOAsyncWriter {
580597

581598
return try await withTaskCancellationHandler {
582599
// We are manually locking here to hold the lock across the withCheckedContinuation call
583-
self._lock.lock()
600+
let unsafe = self._state.unsafe
601+
unsafe.lock()
584602

585-
let action = self._stateMachine.yield(yieldID: yieldID)
603+
let action = unsafe.withValueAssumingLockIsAcquired {
604+
$0.stateMachine.yield(yieldID: yieldID)
605+
}
586606

587607
switch action {
588608
case .callDidYield(let delegate):
589609
// We are allocating a new Deque for every write here
590-
self._lock.unlock()
610+
unsafe.unlock()
591611
delegate.didYield(element)
592612
self.unbufferQueuedEvents()
593613
return .yielded
594614

595615
case .throwError(let error):
596-
self._lock.unlock()
616+
unsafe.unlock()
597617
throw error
598618

599619
case .suspendTask:
600620
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<StateMachine.YieldResult, Error>) in
601-
self._stateMachine.yield(
602-
continuation: continuation,
603-
yieldID: yieldID
604-
)
605-
606-
self._lock.unlock()
607-
self._didSuspend?()
621+
let didSuspend = unsafe.withValueAssumingLockIsAcquired {
622+
$0.stateMachine.yield(continuation: continuation, yieldID: yieldID)
623+
return $0.didSuspend
624+
}
625+
unsafe.unlock()
626+
didSuspend?()
608627
}
609628
}
610629
} onCancel: {
611630
// We must not resume the continuation while holding the lock
612-
// because it can deadlock in combination with the underlying ulock
631+
// because it can deadlock in combination with the underlying lock
613632
// in cases where we race with a cancellation handler
614-
let action = self._lock.withLock {
615-
self._stateMachine.cancel(yieldID: yieldID)
633+
let action = self._state.withLockedValue {
634+
$0.stateMachine.cancel(yieldID: yieldID)
616635
}
617636

618637
switch action {
@@ -630,8 +649,8 @@ extension NIOAsyncWriter {
630649
// We must not resume the continuation while holding the lock
631650
// because it can deadlock in combination with the underlying ulock
632651
// in cases where we race with a cancellation handler
633-
let action = self._lock.withLock {
634-
self._stateMachine.writerFinish(error: error)
652+
let action = self._state.withLockedValue {
653+
$0.stateMachine.writerFinish(error: error)
635654
}
636655

637656
switch action {
@@ -651,8 +670,8 @@ extension NIOAsyncWriter {
651670
// We must not resume the continuation while holding the lock
652671
// because it can deadlock in combination with the underlying ulock
653672
// in cases where we race with a cancellation handler
654-
let action = self._lock.withLock {
655-
self._stateMachine.sinkFinish(error: error)
673+
let action = self._state.withLockedValue {
674+
$0.stateMachine.sinkFinish(error: error)
656675
}
657676

658677
switch action {
@@ -667,7 +686,7 @@ extension NIOAsyncWriter {
667686

668687
@inlinable
669688
/* fileprivate */ internal func unbufferQueuedEvents() {
670-
while let action = self._lock.withLock({ self._stateMachine.unbufferQueuedEvents()}) {
689+
while let action = self._state.withLockedValue({ $0.stateMachine.unbufferQueuedEvents()}) {
671690
switch action {
672691
case .callDidTerminate(let delegate, let error):
673692
delegate.didTerminate(error: error)
@@ -684,12 +703,12 @@ extension NIOAsyncWriter {
684703
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
685704
extension NIOAsyncWriter {
686705
@usableFromInline
687-
/* private */ internal struct StateMachine {
706+
/* private */ internal struct StateMachine: Sendable {
688707
@usableFromInline
689708
typealias YieldID = Storage.YieldIDGenerator.YieldID
690709
/// This is a small helper struct to encapsulate the two different values for a suspended yield.
691710
@usableFromInline
692-
/* private */ internal struct SuspendedYield {
711+
/* private */ internal struct SuspendedYield: Sendable {
693712
/// The yield's ID.
694713
@usableFromInline
695714
var yieldID: YieldID
@@ -715,7 +734,7 @@ extension NIOAsyncWriter {
715734

716735
/// The current state of our ``NIOAsyncWriter``.
717736
@usableFromInline
718-
/* private */ internal enum State: CustomStringConvertible {
737+
/* private */ internal enum State: Sendable, CustomStringConvertible {
719738
/// The initial state before either a call to ``NIOAsyncWriter/yield(contentsOf:)`` or
720739
/// ``NIOAsyncWriter/finish(completion:)`` happened.
721740
case initial(

0 commit comments

Comments
 (0)