@@ -158,4 +158,82 @@ public void Run(Action task)
158158 } , this ) ;
159159 }
160160 }
161+
162+ /// <summary>
163+ /// A stack of actions drained by up to <see cref="MaxConcurrency"/> threads at once, for work
164+ /// that is independent per item and too expensive to serialise - decoding a panel's worth of
165+ /// stickers, where one at a time turns a few milliseconds each into seconds of them arriving
166+ /// one by one.
167+ /// </summary>
168+ /// <remarks>
169+ /// A stack rather than a queue: the most recently pushed item is the one nearest what the user
170+ /// is looking at, so taking from the back follows a scroll instead of trailing it.
171+ ///
172+ /// A failing action is logged and the drain continues. Abandoning the rest of the queue
173+ /// because one item threw would strand everything behind it.
174+ /// </remarks>
175+ public partial class ParallelActionWorker
176+ {
177+ private readonly ConcurrentStack < Action > taskQueue = new ( ) ;
178+ private int _concurrentCount = 0 ;
179+
180+ public int MaxConcurrency { get ; }
181+
182+ public ParallelActionWorker ( int maxConcurrency )
183+ {
184+ MaxConcurrency = Math . Max ( 1 , maxConcurrency ) ;
185+ }
186+
187+ public void Run ( Action task )
188+ {
189+ taskQueue . Push ( task ) ;
190+ TryStartDrain ( ) ;
191+ }
192+
193+ private void TryStartDrain ( )
194+ {
195+ var count = Volatile . Read ( ref _concurrentCount ) ;
196+
197+ while ( count < MaxConcurrency )
198+ {
199+ var previous = Interlocked . CompareExchange ( ref _concurrentCount , count + 1 , count ) ;
200+ if ( previous == count )
201+ {
202+ ThreadPool . UnsafeQueueUserWorkItem ( static state => ( ( ParallelActionWorker ) state ! ) . Drain ( ) , this ) ;
203+ return ;
204+ }
205+
206+ count = previous ;
207+ }
208+ }
209+
210+ private void Drain ( )
211+ {
212+ try
213+ {
214+ while ( taskQueue . TryPop ( out var next ) )
215+ {
216+ try
217+ {
218+ next ( ) ;
219+ }
220+ catch ( Exception ex )
221+ {
222+ Logger . Error ( ex ) ;
223+ }
224+ }
225+ }
226+ finally
227+ {
228+ Interlocked . Decrement ( ref _concurrentCount ) ;
229+
230+ // A push that landed between the pop that failed and the decrement above would
231+ // otherwise sit in the stack with nobody left to drain it.
232+ if ( ! taskQueue . IsEmpty )
233+ {
234+ TryStartDrain ( ) ;
235+ }
236+ }
237+ }
238+ }
161239}
0 commit comments