2828
2929namespace Dispatch
3030{
31- /// <summary>Implements a serial queue</summary>
31+ /// <summary>
32+ /// This class is the main purpose of the library.
33+ /// It represents a serial queue which will run all it's callbacks sequentially and safely
34+ /// (like a thread) but whose execution actually is performed on the OS threadpool.</summary>
3235 public class SerialQueue : IDispatchQueue
3336 {
3437 enum AsyncState
@@ -54,8 +57,12 @@ enum AsyncState
5457 volatile AsyncState m_asyncState = AsyncState . Idle ; // acquire m_schedulerLock
5558 bool m_isDisposed = false ; // acquire m_schedulerLock
5659
57- /// <summary>Constructs a new SerialQueue backed by the given ThreadPool</summary>
60+ /// <summary>Constructs a new SerialQueue backed by a custom ThreadPool implementation.
61+ /// This primarily exists to enable unit testing, however if you have a custom ThreadPool you could use it here</summary>
5862 /// <param name="threadpool">The threadpool to queue async actions to</param>
63+ /// <param name="name">An optional friendly name for this queue</param>
64+ /// <param name="features">You may opt-out of certain features in order to reduce overhead.
65+ /// You shouldn't need to do this except in extreme situations as shown by profiling.</param>
5966 public SerialQueue ( IThreadPool threadpool , string ? name = null , SerialQueueFeatures features = SerialQueueFeatures . All )
6067 {
6168 m_threadPool = threadpool ?? throw new ArgumentNullException ( nameof ( threadpool ) ) ;
@@ -66,10 +73,14 @@ public SerialQueue(IThreadPool threadpool, string? name = null, SerialQueueFeatu
6673 m_syncContext = new DispatchQueueSynchronizationContext ( this ) ;
6774 }
6875
69- /// <summary>Constructs a new SerialQueue backed by the default TaskThreadPool</summary>
76+ /// <summary>Constructs a new SerialQueue backed by the default TaskThreadPool.
77+ /// This is the default constructor which is intended for normal use</summary>
78+ /// <param name="name">An optional friendly name for this queue</param>
79+ /// <param name="features">You may opt-out of certain features in order to reduce overhead.
80+ /// You shouldn't need to do this except in extreme situations as shown by profiling.</param>
7081 public SerialQueue ( string ? name = null , SerialQueueFeatures features = SerialQueueFeatures . All ) : this ( TaskThreadPool . Default , name , features ) { }
7182
72- /// <summary>Returns the name (if one is set)</summary>
83+ /// <summary>Returns the friendly name (if one is set)</summary>
7384 public string ? Name { get ; }
7485
7586 /// <summary>Returns the enabled features this serial queue has</summary>
@@ -91,6 +102,96 @@ public void VerifyQueue()
91102 throw new InvalidOperationException ( "On the wrong queue" ) ;
92103 }
93104
105+ /// <summary>Schedules the given action to run asynchronously on the queue when it is available</summary>
106+ /// <param name="action">The function to run</param>
107+ /// <returns>A disposable token which you can use to cancel the async action if it has not run yet.
108+ /// It is always safe to dispose this token, even if the async action has already run</returns>
109+ public virtual IDisposable DispatchAsync ( Action action )
110+ {
111+ lock ( m_schedulerLock )
112+ {
113+ if ( m_isDisposed )
114+ throw new ObjectDisposedException ( nameof ( SerialQueue ) , "Cannot call DispatchAsync on a disposed queue" ) ;
115+
116+ m_asyncActions . Add ( action ) ;
117+
118+ if ( m_asyncState == AsyncState . Idle )
119+ {
120+ // even though we don't hold m_schedulerLock when asyncActionsAreProcessing is set to false
121+ // that should be OK as the only "contention" happens up here while we do hold it
122+ m_asyncState = AsyncState . Scheduled ;
123+ m_threadPool . QueueWorkItem ( ProcessAsync ) ;
124+ }
125+ }
126+
127+ return new AnonymousDisposable ( ( ) => {
128+ // we can't "take it out" of the threadpool as not all threadpools support that
129+ lock ( m_schedulerLock )
130+ m_asyncActions . Remove ( action ) ;
131+ } ) ;
132+ }
133+
134+ /// <summary>Runs the given action on the queue.
135+ /// Blocks until the action is fully complete.
136+ /// If the queue is not currently busy processing asynchronous actions (a very common state), this should have the same performance characteristics as a simple lock, so it is often nice and convenient.
137+ /// The SerialQueue guarantees the action will run on the calling thread(it will NOT thread-jump).
138+ /// Other implementations of IDispatchQueue reserve the right to run the action on a different thread(e.g WPF Dispatcher)</summary>
139+ /// <param name="action">The function to run.</param>
140+ public virtual void DispatchSync ( Action action )
141+ {
142+ var prevStack = s_queueStack . Value . ToArray ( ) ; // there might be a more optimal way of doing this, it seems to be fast enough
143+ s_queueStack . Value . Push ( this ) ;
144+
145+ bool schedulerLockTaken = false ;
146+ try
147+ {
148+ Monitor . Enter ( m_schedulerLock , ref schedulerLockTaken ) ;
149+ Debug . Assert ( schedulerLockTaken ) ;
150+
151+ if ( m_isDisposed )
152+ throw new ObjectDisposedException ( nameof ( SerialQueue ) , "Cannot call DispatchSync on a disposed queue" ) ;
153+
154+ if ( m_asyncState == AsyncState . Idle || prevStack . Contains ( this ) ) // either queue is empty or it's a nested call
155+ {
156+ Monitor . Exit ( m_schedulerLock ) ;
157+ schedulerLockTaken = false ;
158+
159+ // process the action
160+ lock ( m_executionLock )
161+ action ( ) ; // DO NOT CATCH EXCEPTIONS. We're excuting synchronously so just let it throw
162+ return ;
163+ }
164+
165+ // if there is any async stuff scheduled we must also schedule
166+ // else m_asyncState == AsyncState.Scheduled, OR we fell through from Processing
167+ var asyncReady = new ManualResetEvent ( false ) ;
168+ var syncDone = new ManualResetEvent ( false ) ;
169+ DispatchAsync ( ( ) => {
170+ asyncReady . Set ( ) ;
171+ syncDone . WaitOne ( ) ;
172+ } ) ;
173+ Monitor . Exit ( m_schedulerLock ) ;
174+ schedulerLockTaken = false ;
175+
176+ try
177+ {
178+ asyncReady . WaitOne ( ) ;
179+ action ( ) ; // DO NOT CATCH EXCEPTIONS. We're excuting synchronously so just let it throw
180+ }
181+ finally
182+ {
183+ syncDone . Set ( ) ; // tell the dispatchAsync it can release the lock
184+ }
185+ }
186+ finally
187+ {
188+ if ( schedulerLockTaken )
189+ Monitor . Exit ( m_schedulerLock ) ;
190+
191+ s_queueStack . Value . Pop ( ) ; // technically we leak the queue stack threadlocal, but it's probably OK. Windows will free it when the thread exits
192+ }
193+ }
194+
94195 /// <summary>Schedules the given action to run asynchronously on the queue after dueTime.</summary>
95196 /// <remarks>The function is not guaranteed to run at dueTime as the queue may be busy, it will run when next able.</remarks>
96197 /// <param name="dueTime">Delay before running the action</param>
@@ -137,35 +238,6 @@ public virtual IDisposable DispatchAfter(TimeSpan dueTime, Action action)
137238 }
138239 } ) ;
139240 }
140-
141- /// <summary>Schedules the given action to run asynchronously on the queue when it is available</summary>
142- /// <param name="action">The function to run</param>
143- /// <returns>A disposable token which you can use to cancel the async action if it has not run yet.
144- /// It is always safe to dispose this token, even if the async action has already run</returns>
145- public virtual IDisposable DispatchAsync ( Action action )
146- {
147- lock ( m_schedulerLock )
148- {
149- if ( m_isDisposed )
150- throw new ObjectDisposedException ( nameof ( SerialQueue ) , "Cannot call DispatchAsync on a disposed queue" ) ;
151-
152- m_asyncActions . Add ( action ) ;
153-
154- if ( m_asyncState == AsyncState . Idle )
155- {
156- // even though we don't hold m_schedulerLock when asyncActionsAreProcessing is set to false
157- // that should be OK as the only "contention" happens up here while we do hold it
158- m_asyncState = AsyncState . Scheduled ;
159- m_threadPool . QueueWorkItem ( ProcessAsync ) ;
160- }
161- }
162-
163- return new AnonymousDisposable ( ( ) => {
164- // we can't "take it out" of the threadpool as not all threadpools support that
165- lock ( m_schedulerLock )
166- m_asyncActions . Remove ( action ) ;
167- } ) ;
168- }
169241
170242 /// <summary>Internal function which runs on the threadpool to execute the actual async actions</summary>
171243 protected virtual void ProcessAsync ( )
@@ -235,65 +307,6 @@ protected virtual void ProcessAsync()
235307 }
236308 }
237309
238- /// <summary>Runs the given action on the queue.
239- /// Blocks until the action is fully complete.
240- /// This implementation will not switch threads to run the function</summary>
241- /// <param name="action">The function to run.</param>
242- public virtual void DispatchSync ( Action action )
243- {
244- var prevStack = s_queueStack . Value . ToArray ( ) ; // there might be a more optimal way of doing this, it seems to be fast enough
245- s_queueStack . Value . Push ( this ) ;
246-
247- bool schedulerLockTaken = false ;
248- try
249- {
250- Monitor . Enter ( m_schedulerLock , ref schedulerLockTaken ) ;
251- Debug . Assert ( schedulerLockTaken ) ;
252-
253- if ( m_isDisposed )
254- throw new ObjectDisposedException ( nameof ( SerialQueue ) , "Cannot call DispatchSync on a disposed queue" ) ;
255-
256- if ( m_asyncState == AsyncState . Idle || prevStack . Contains ( this ) ) // either queue is empty or it's a nested call
257- {
258- Monitor . Exit ( m_schedulerLock ) ;
259- schedulerLockTaken = false ;
260-
261- // process the action
262- lock ( m_executionLock )
263- action ( ) ; // DO NOT CATCH EXCEPTIONS. We're excuting synchronously so just let it throw
264- return ;
265- }
266-
267- // if there is any async stuff scheduled we must also schedule
268- // else m_asyncState == AsyncState.Scheduled, OR we fell through from Processing
269- var asyncReady = new ManualResetEvent ( false ) ;
270- var syncDone = new ManualResetEvent ( false ) ;
271- DispatchAsync ( ( ) => {
272- asyncReady . Set ( ) ;
273- syncDone . WaitOne ( ) ;
274- } ) ;
275- Monitor . Exit ( m_schedulerLock ) ;
276- schedulerLockTaken = false ;
277-
278- try
279- {
280- asyncReady . WaitOne ( ) ;
281- action ( ) ; // DO NOT CATCH EXCEPTIONS. We're excuting synchronously so just let it throw
282- }
283- finally
284- {
285- syncDone . Set ( ) ; // tell the dispatchAsync it can release the lock
286- }
287- }
288- finally
289- {
290- if ( schedulerLockTaken )
291- Monitor . Exit ( m_schedulerLock ) ;
292-
293- s_queueStack . Value . Pop ( ) ; // technically we leak the queue stack threadlocal, but it's probably OK. Windows will free it when the thread exits
294- }
295- }
296-
297310 /// <summary>Shuts down the queue. All unstarted async actions will be dropped,
298311 /// and any future attempts to call one of the Dispatch functions will throw an
299312 /// ObjectDisposedException</summary>
@@ -321,8 +334,12 @@ protected virtual void Dispose(bool disposing)
321334 }
322335 }
323336
337+ /// <summary>Enables capture of a serial queue as a SynchronizationContext for async/await.
338+ /// You shouldn't need to interact with this class yourself</summary>
324339 public class DispatchQueueSynchronizationContext : SynchronizationContext
325340 {
341+ /// <summary>Constructs a new DispatchQueueSynchronizationContext wrapping the given queue</summary>
342+ /// <param name="queue">Queue to post actions to</param>
326343 public DispatchQueueSynchronizationContext ( IDispatchQueue queue )
327344 => Queue = queue ;
328345
@@ -335,15 +352,19 @@ public override void Send(SendOrPostCallback d, object state)
335352 => Queue . DispatchSync ( ( ) => d ( state ) ) ;
336353 }
337354
338- // Use these to turn on and off various features of the serial queue for performance reasons
355+ /// <summary>
356+ /// Use these to turn on and off various features of the serial queue for performance reasons
357+ /// </summary>
339358 [ Flags ]
340359 public enum SerialQueueFeatures
341360 {
361+ /// <summary>Only basic functionality</summary>
342362 None = 0 ,
343- // Note: if there is a need for it, we could put the Verify/Re-entrant DispatchSync behaviour behind a feature
344- // which could improve performance significantly
363+
364+ /// <summary>If enabled, you may use this queue with async/await</summary>
345365 SynchronizationContext = 1 ,
346366
367+ /// <summary>All features enabled</summary>
347368 All = SynchronizationContext
348369 }
349370}
0 commit comments