Skip to content
Orion Edwards edited this page Feb 13, 2020 · 7 revisions

public class SerialQueue : IDispatchQueue

This class is the main purpose of the library. It represents a serial queue which will run all it's callbacks sequentially and safely (like a thread) but whose execution actually is performed on the OS threadpool.

SerialQueue(string? name = null, SerialQueueFeatures features = SerialQueueFeatures.All)

Constructs a new SerialQueue backed by the default TaskThreadPool. This is the default constructor which is intended for normal use

Parameter name: An optional friendly name for this queue
Parameter features: You may opt-out of certain features in order to reduce overhead. You shouldn't need to do this except in extreme situations as shown by profiling

SerialQueue(IThreadPool threadpool, string? name = null, SerialQueueFeatures features = SerialQueueFeatures.All)

Constructs a new SerialQueue backed by a custom ThreadPool implementation. This primarily exists to enable unit testing, however if you have a custom ThreadPool you could use it here

Parameter threadpool: The threadpool to queue async actions to
Parameter name: An optional friendly name for this queue
Parameter features: You may opt-out of certain features in order to reduce overhead. You shouldn't need to do this except in extreme situations as shown by profiling

Constructor which allows you to specify a custom threadpool implementation for advanced scenarios. See IThreadPool

string? Name { get; }

Returns the friendly name (if one is set)

SerialQueueFeatures Features { get; }

Returns the enabled features this serial queue has

event EventHandler<UnhandledExceptionEventArgs>? UnhandledException

This event is raised whenever an asynchronous callback function (via DispatchAsync or DispatchAfter) throws an unhandled exception

static SerialQueue? Current

Returns the topmost queue that we are currently executing on, or null if we're not on any queue.
Note this only works for serial queues specifically, it doesn't generalize to any IDispatchQueue

void VerifyQueue()

Checks whether the currently-executing function is on this queue, and throw an OperationInvalidException if it is not

IDisposable DispatchAsync(Action action)

Schedules the given action to run asynchronously on the queue when it is available

Parameter action: The function to run
Returns A disposable token which you can use to cancel the async action if it has not run yet. It is always safe to dispose this token, even if the async action has already run

void DispatchSync(Action action)

Runs the given action on the queue.
Blocks until the action is fully complete.

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.
The SerialQueue guarantees the action will run on the calling thread (it will NOT thread-jump).
Other implementations of IDispatchQueue reserve the right to run the action on a different thread (e.g WPF Dispatcher)

Parameter action: The function to run

IDisposable DispatchAfter(TimeSpan dueTime, Action action)

Schedules the given action to run asynchronously on the queue after dueTime.
The function is not guaranteed to run at dueTime as the queue may be busy, it will run when next able.

Parameter dueTime: Delay before running the action
Parameter `action": The function to run
Returns A disposable token which you can use to cancel the async action if it has not run yet.
It is always safe to dispose this token, even if the async action has already run

void Dispose()

Shuts down the queue. All unstarted async actions will be dropped, and any future attempts to call one of the Dispatch functions will throw an ObjectDisposedException

public class DispatchQueueSynchronizationContext : SynchronizationContext

Enables capture of a serial queue as a SynchronizationContext for async/await.
You shouldn't need to interact with this class yourself

DispatchQueueSynchronizationContext(IDispatchQueue queue)

Constructs a new DispatchQueueSynchronizationContext wrapping the given queue

Parameter queue: Queue to post actions to

[Flags] enum SerialQueueFeatures

Use these to turn on and off various features of the serial queue for performance reasons

None: Only basic functionality SynchronizationContext: If enabled, you may use this queue with async/await All: All features enabled

public static class IDispatchQueueExtensions

Useful extension methods for queues

static T DispatchSync<T>(this IDispatchQueue queue, Func<T> func)

A wrapper over DispatchSync that calls a value-producing function and returns it's result

static Task<T> DispatchAsync<T>(this IDispatchQueue queue, Func<T> func)

A wrapper over DispatchAsync that calls a value-producing function on the queue and returns it's result via a Task

Note: the task "completes" on the dispatch queue, so if you use ConfigureAwait(false) or TaskContinuationOptions.ExecuteSychronously, you will still be running on the dispatch queue

static DispatchQueueAwaiter GetAwaiter(this IDispatchQueue queue)

This allows you await directly on a queue, which is handy for queue-jumping if you are already in the middle of an async method.

public interface IDispatchQueue : IDisposable

The SerialQueue class implements the IDispatchQueue interface, which provides a basic abstraction over a queue. It is often very useful to create other kinds of queues linked to UI threads, other custom threads, etc so you can present your application code with a uniform interface by using the IDispatchQueue interface rather than a SerialQueue directly. For example, I have often used a DispatcherQueue in WPF apps, which is an implementation of IDispatchQueue that just forwards methods to the WPF Dispatcher (representing the UI thread).

The IDispatchQueue interface requires these methods, which should behave in a similar fashion to the SerialQueue

IDisposable DispatchAsync(Action action)
void DispatchSync(Action action)
IDisposable DispatchAfter(TimeSpan dueTime, Action action)
void VerifyQueue()

You may chose not to implement some of these methods (e.g. VerifyQueue) if you never need them

IThreadPool Interface

The SerialQueue abstracts the underlying threadpool with this interface. It is intended to support unit testing as well as advanced behaviour. For example, if you do not wish your serial queues to post callbacks to the built in .NET threadpool, and would rather use a custom threadpool, you can achieve this using a custom implementation of IThreadPool

You might want to do this if, for example you post a lot of long-running operations to your serial queues (the default CLR threadpool doesn't deal with lots of long-running operations very well) or if you wish to limit the amount of things that run in parallel (The default CLR threadpool will use all the cores in your machine, you might wish to limit that to say 1 or 2 underlying OS threads)

void QueueWorkItem(Action action)

The serial queue uses this to post an async action to the threadpool

Parameter action: The function to run. Note this function will be an internal SerialQueue worker function, not a direct user-supplied function

IDisposable Schedule(TimeSpan dueTime, Action action)

The serial queue uses this to create a timer. The timer doesn't have to be related to the the threadpool, it is only in the IThreadPool interface so the queue can have one dependency rather than 2.

Parameter dueTime: When the timer should fire
Parameter action: The function to run after the timer elapses
Returns A disposable token to cancel the timer

If you are creating your own implementation of IThreadPool and don't need any custom timer behaviour, please just use the default implementation which is:

public IDisposable Schedule(TimeSpan dueTime, Action action)
{ return new Timer(_ => action(), null, dueTime, TimeSpan.FromMilliseconds(-1)); }