Skip to content

Commit b72fe44

Browse files
committed
Removed the explicit locking for interruptedState.
1 parent d1155d6 commit b72fe44

File tree

1 file changed

+32
-18
lines changed

1 file changed

+32
-18
lines changed

Sources/Signal.swift

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ public final class Signal<Value, Error: Swift.Error> {
4141
state = Atomic(SignalState())
4242

4343
/// Holds the final signal state captured by an `interrupted` event. If it
44-
/// is set, the Signal should interrupt as soon as possible.
45-
let interruptedState = Atomic<SignalState<Value, Error>?>(nil)
44+
/// is set, the Signal should interrupt as soon as possible. Implicitly
45+
/// protected by `state` and `sendLock`.
46+
var interruptedState: SignalState<Value, Error>? = nil
4647

4748
/// Used to track if the signal has terminated. Protected by `sendLock`.
4849
var terminated = false
@@ -56,6 +57,15 @@ public final class Signal<Value, Error: Swift.Error> {
5657
return
5758
}
5859

60+
@inline(__always)
61+
func interrupt(_ observers: Bag<Observer>) {
62+
for observer in observers {
63+
observer.sendInterrupted()
64+
}
65+
terminated = true
66+
interruptedState = nil
67+
}
68+
5969
if case .interrupted = event {
6070
// Recursive events are generally disallowed. But `interrupted` is kind
6171
// of a special snowflake, since it can inadvertently be sent by
@@ -66,14 +76,24 @@ public final class Signal<Value, Error: Swift.Error> {
6676
// the disposal would be delegated to the current sender, or
6777
// occasionally one of the senders waiting on `sendLock`.
6878
if let state = signal.state.swap(nil) {
69-
interruptedState.value = state
79+
// Writes to `interruptedState` are implicitly synchronized. So we do
80+
// not need to guard it with locks.
81+
//
82+
// Specifically, senders serialized by `sendLock` can react to and
83+
// clear `interruptedState` only if they see the write made below.
84+
// The write can happen only once, since `state` being swapped with
85+
// `nil` is a point of no return.
86+
//
87+
// Even in the case that both a previous sender and its successor see
88+
// the write (the `interruptedState` check before & after the unlock
89+
// of `sendLock`), the senders are still bound to the `sendLock`.
90+
// So whichever sender loses the battle of acquring `sendLock` is
91+
// guaranteed to be blocked.
92+
interruptedState = state
7093

7194
if sendLock.try() {
72-
if !terminated, let state = interruptedState.swap(nil) {
73-
for observer in state.observers {
74-
observer.sendInterrupted()
75-
}
76-
terminated = true
95+
if !terminated, let state = interruptedState {
96+
interrupt(state.observers)
7797
}
7898
sendLock.unlock()
7999
signal.generatorDisposable?.dispose()
@@ -94,11 +114,8 @@ public final class Signal<Value, Error: Swift.Error> {
94114

95115
// Check if a downstream consumer or a concurrent sender has
96116
// interrupted the signal.
97-
if !isTerminating, let state = interruptedState.swap(nil) {
98-
for observer in state.observers {
99-
observer.sendInterrupted()
100-
}
101-
terminated = true
117+
if !isTerminating, let state = interruptedState {
118+
interrupt(state.observers)
102119
shouldDispose = true
103120
}
104121

@@ -114,15 +131,12 @@ public final class Signal<Value, Error: Swift.Error> {
114131
// `interruptedState` should always be visible after `sendLock` is
115132
// released. So we check it again and handle the interruption if
116133
// it has not been taken over.
117-
if !shouldDispose && !terminated && !isTerminating, let state = interruptedState.swap(nil) {
134+
if !shouldDispose && !terminated && !isTerminating, let state = interruptedState {
118135
sendLock.lock()
119136

120137
if !terminated {
121-
for observer in state.observers {
122-
observer.sendInterrupted()
123-
}
138+
interrupt(state.observers)
124139
shouldDispose = true
125-
terminated = true
126140
}
127141

128142
sendLock.unlock()

0 commit comments

Comments
 (0)