@@ -73,97 +73,29 @@ public class Publisher {
73
73
backend: Backend ,
74
74
action closure: @escaping ( ) -> Void
75
75
) -> Cancellable {
76
- // All of the concurrency related code is there to detect when updates can be merged
77
- // together (a.k.a. when one of the updates is unnecessary).
78
- let protectingQueue = DispatchQueue ( label: " state update merging " )
79
- let concurrentUpdateHandlingQueue = DispatchQueue (
80
- label: " concurrent update handling queue " ,
81
- attributes: . concurrent
76
+ let serialUpdateHandlingQueue = DispatchQueue (
77
+ label: " serial update handling "
82
78
)
83
- let synchronizationSemaphore = DispatchSemaphore ( value: 1 )
84
-
85
- // State shared betwen all calls to the closure defined below.
86
- var updateIsQueued = false
87
- var updateIsRunning = false
88
- var aCurrentJobDidntHaveToWait = false
89
-
79
+ let semaphore = DispatchSemaphore ( value: 1 )
90
80
return observe {
91
- // I'm sorry if you have to make sense of this... Take my comments as a peace offering.
92
-
93
- // Hop to a dispatch queue to avoid blocking any threads in the Swift Structured
94
- // Concurrency thread pool in the case that the state update originated from a task.
95
- concurrentUpdateHandlingQueue. async {
96
- // If no one is running, then we run without waiting, and if someone's running
97
- // but no one's waiting, then we wait and prevent anyone else from waiting.
98
- // This ensures that at least one update will always happen after every update
99
- // received so far, without letting unnecessary updates queue up. The reason
100
- // that we can merge updates like this is that all state updates are built equal;
101
- // they don't carry any information other than that they happened.
102
- var shouldWait = false
103
- protectingQueue. sync {
104
- if !updateIsQueued {
105
- shouldWait = true
106
- }
107
-
108
- if updateIsRunning {
109
- updateIsQueued = true
110
- } else {
111
- updateIsRunning = true
112
- aCurrentJobDidntHaveToWait = true
113
- }
114
- }
115
-
116
- guard shouldWait else {
117
- return
118
- }
81
+ // Only allow one update to wait at a time.
82
+ guard semaphore. wait ( timeout: . now( ) ) == . success else {
83
+ return
84
+ }
119
85
120
- // Waiting just involves attempting to jump to the main thread.
86
+ // Add update to queue. We use our own serial update handling queue since some
87
+ // backends don't have the concept of a main thread, leading to the possibility
88
+ // that two updates can run at once which would be inefficient and lead to
89
+ // incorrect results anyway.
90
+ serialUpdateHandlingQueue. async {
121
91
backend. runInMainThread {
122
- // This semaphore is used because some backends don't put us on the main
123
- // thread since they don't have the concept of a single UI thread like
124
- // macOS does.
125
- //
126
- // If `backend.runInMainThread` is truly putting us on the main thread,
127
- // then this never have to block significantly, otherwise we're just
128
- // blocking some random thread, so either way we're fine since we've
129
- // explicitly hopped to a dispatch queue to escape any cooperative
130
- // Swift Structured Concurrency thread pool the state update may have
131
- // originated from.
132
- synchronizationSemaphore. wait ( )
133
-
134
- protectingQueue. sync {
135
- // If a current job didn't have to wait, then that's us. Due to
136
- // concurrency that doesn't mean we were the first update triggered.
137
- // That is, we could've been the job that set `updateIsQueued` to
138
- // true while still being the job that reached this line first (before
139
- // the one that set `updateIsRunning` to true). And that's why I've
140
- // implemented the check in this way with a protected 'global' and not
141
- // a local variable (being first isn't a property we can know ahead
142
- // of time). I use 'global' in the sense of shared between all calls
143
- // to the state update handling closure for a given ViewGraphNode.
144
- //
145
- // The reason that `aCurrentJobDidntHaveToWait` is needed at all is
146
- // so that we can know whether `updateIsQueued`'s value is due to us
147
- // or someone else/no one.
148
- if aCurrentJobDidntHaveToWait {
149
- aCurrentJobDidntHaveToWait = false
150
- } else {
151
- updateIsQueued = false
152
- }
153
- }
92
+ // Now that we're about to start, let another update queue up. If we
93
+ // instead waited until we're finished the update, we'd introduce the
94
+ // possibility of dropping updates that would've affected views that
95
+ // we've already processed, leading to stale view contents.
96
+ semaphore. signal ( )
154
97
155
98
closure ( )
156
-
157
- // If someone is waiting then we leave `updateIsRunning` equal to true
158
- // because they'll immediately begin running as soon as we exit and we
159
- // don't want an extra person queueing until they've actually started.
160
- protectingQueue. sync {
161
- if !updateIsQueued {
162
- updateIsRunning = false
163
- }
164
- }
165
-
166
- synchronizationSemaphore. signal ( )
167
99
}
168
100
}
169
101
}
0 commit comments