Skip to content

Commit ed77960

Browse files
Documentation sync with github
NO CODE CHANGES
1 parent 0ed6f9c commit ed77960

File tree

1 file changed

+116
-95
lines changed

1 file changed

+116
-95
lines changed

src/SerialQueue.cs

Lines changed: 116 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@
2828

2929
namespace 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

Comments
 (0)