From 6db79105f8c8534fc1f35564291ed42822e0e49a Mon Sep 17 00:00:00 2001 From: Matthias Vill Date: Thu, 25 Jun 2026 21:25:43 +0200 Subject: [PATCH] Let the user change the element-queue for AsyncQueue This allows the user to prioritize queued elements, or use an implementation otherwise different from Queue as backend. --- .../AsyncQueue`1.cs | 20 ++++++- .../ISyncQueue`1.cs | 41 ++++++++++++++ .../SyncQueue`1.cs | 28 ++++++++++ .../AsyncQueueTests.cs | 56 +++++++++++++++++++ 4 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 src/Microsoft.VisualStudio.Threading/ISyncQueue`1.cs create mode 100644 src/Microsoft.VisualStudio.Threading/SyncQueue`1.cs diff --git a/src/Microsoft.VisualStudio.Threading/AsyncQueue`1.cs b/src/Microsoft.VisualStudio.Threading/AsyncQueue`1.cs index 10c7624ba..c9da1b29a 100644 --- a/src/Microsoft.VisualStudio.Threading/AsyncQueue`1.cs +++ b/src/Microsoft.VisualStudio.Threading/AsyncQueue`1.cs @@ -32,10 +32,15 @@ public class AsyncQueue : ThreadingTools.ICancellationNotification /// private volatile TaskCompletionSource? completedSource; + /// + /// The factory for the internal queue of elements. A Queue`1 is constructed, when this is null. + /// + private readonly Func>? elementsQueueFactory; + /// /// The internal queue of elements. Lazily constructed. /// - private Queue? queueElements; + private ISyncQueue? queueElements; /// /// The internal queue of waiters. Lazily constructed. @@ -59,6 +64,15 @@ public AsyncQueue() { } + /// + /// Initializes a new instance of the class. + /// + /// Factory to create the queue used for items, if needed. Takes the initial capacity. + public AsyncQueue(Func> elementsQueueFactory) + { + this.elementsQueueFactory = elementsQueueFactory; + } + /// /// Gets a value indicating whether the queue is currently empty. /// @@ -202,7 +216,9 @@ public bool TryEnqueue(T value) { if (this.queueElements is null) { - this.queueElements = new Queue(this.InitialCapacity); + this.queueElements = this.elementsQueueFactory is not null + ? this.elementsQueueFactory(this.InitialCapacity) + : new SyncQueue(this.InitialCapacity); } this.queueElements.Enqueue(value); diff --git a/src/Microsoft.VisualStudio.Threading/ISyncQueue`1.cs b/src/Microsoft.VisualStudio.Threading/ISyncQueue`1.cs new file mode 100644 index 000000000..1e1745626 --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading/ISyncQueue`1.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// A synchronously dequeuable queue. There are no thread-safety guarantees. +/// +/// The type of values kept by the queue. +public interface ISyncQueue +{ + /// + /// Gets the number of elements currently in the queue. + /// + int Count { get; } + + /// + /// Adds an element to the tail of the queue. + /// + /// The value to add. + void Enqueue(T value); + + /// + /// Gets the value at the head of the queue without removing it from the queue. + /// + /// Thrown if the queue is empty. + T Peek(); + + /// + /// Gets and removes the element at the head of the queue. + /// + /// The head element. + T Dequeue(); + + /// + /// Returns a copy of this queue as an array. + /// + T[] ToArray(); +} diff --git a/src/Microsoft.VisualStudio.Threading/SyncQueue`1.cs b/src/Microsoft.VisualStudio.Threading/SyncQueue`1.cs new file mode 100644 index 000000000..d0f02fbc9 --- /dev/null +++ b/src/Microsoft.VisualStudio.Threading/SyncQueue`1.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.VisualStudio.Threading; + +/// +/// A non-thread-safe wrapper around Queue. +/// +/// The type of values kept by the queue. +[DebuggerDisplay("Count = {Count}")] +internal sealed class SyncQueue(int capacity) : ISyncQueue +{ + private readonly Queue queue = new(capacity); + + public int Count => this.queue.Count; + + public void Enqueue(T value) => this.queue.Enqueue(value); + + public T Peek() => this.queue.Peek(); + + public T Dequeue() => this.queue.Dequeue(); + + public T[] ToArray() => this.queue.ToArray(); +} + diff --git a/test/Microsoft.VisualStudio.Threading.Tests/AsyncQueueTests.cs b/test/Microsoft.VisualStudio.Threading.Tests/AsyncQueueTests.cs index c53a34c2c..fbc25b76b 100644 --- a/test/Microsoft.VisualStudio.Threading.Tests/AsyncQueueTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Tests/AsyncQueueTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; @@ -576,6 +577,24 @@ public void DequeueAsyncContinuationsNotInlinedWithinPrivateLock() continuationTask.Wait(UnexpectedTimeout); } + [Fact] + public async Task AsyncQueueCanUseACustomQueueForElements() + { + var asyncQueue = new AsyncQueue(capacity => new PrioritizedQueue(capacity)); + + asyncQueue.Enqueue(3); + asyncQueue.Enqueue(1); + asyncQueue.Enqueue(2); + asyncQueue.Complete(); + + Assert.Equal(1, asyncQueue.Peek()); + Assert.Equal(1, await asyncQueue.DequeueAsync()); + Assert.Equal(2, await asyncQueue.DequeueAsync()); + Assert.True(asyncQueue.TryDequeue(out int item)); + Assert.Equal(3, item); + Assert.False(asyncQueue.TryDequeue(out item)); + } + private class DerivedQueue : AsyncQueue { internal Action? OnEnqueuedDelegate { get; set; } @@ -605,4 +624,41 @@ protected override void OnCompleted() this.OnCompletedDelegate?.Invoke(); } } + + /// + /// This "prioritied" queue implementation is not optimized for speed, nor does it allow custom types or ordering. + /// + private class PrioritizedQueue(int capacity) : ISyncQueue + { + private readonly List items = new List(capacity); + + public int Count => this.items.Count; + + public void Enqueue(int value) + { + int index = this.items.BinarySearch(value); + + // for more interesting types, there would be an `else`, moving to the end of items with the same priority, but you can't tell `int`s apart anyway. + if (index < 0) + { + index = ~index; + } + + this.items.Insert(index, value); + } + + public int Peek() => this.items[0]; + + public int Dequeue() + { + var item = this.items[0]; + this.items.RemoveAt(0); + return item; + } + + public int[] ToArray() + { + return this.items.ToArray(); + } + } }