Skip to content

Commit 0ed6f9c

Browse files
authored
Merge pull request #3 from borland/orion/await-and-names
Can await the queue itself directly, can do SerialQueue.Current, and …
2 parents 5a3d574 + fbf9534 commit 0ed6f9c

File tree

7 files changed

+261
-37
lines changed

7 files changed

+261
-37
lines changed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) 2015 Orion Edwards
3+
Copyright (c) 2020 Orion Edwards
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# SerialQueue
22
C# Implementation of a SerialQueue in the style of Apple's Grand Central Dispatch queues.
33

4-
Provided as a portable class library targeting .NET 4.5.1 / Windows 8.1 or newer.
4+
Provided as a Nuget package built for Net462 and NetStandard20
55

6-
##What is a Serial Queue?
6+
## What is a Serial Queue?
77

88
[Apple's Grand Central Dispatch library](https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html)
99
provides three kinds of "operation queues".
@@ -19,7 +19,7 @@ Serial queues on the other hand have no built-in equivalent. Apple's documentati
1919
> Serial queues (also known as private dispatch queues) execute one task at a time in the order in which they are added to the queue. The currently executing task runs on a distinct thread (which can vary from task to task) that is managed by the dispatch queue. Serial queues are often used to synchronize access to a specific resource.
2020
> You can create as many serial queues as you need, and each queue operates concurrently with respect to all other queues. In other words, if you create four serial queues, each queue executes only one task at a time but up to four tasks could still execute concurrently, one from each queue.
2121
22-
##Why would I want to use one?
22+
## Why would I want to use one?
2323

2424
There are quite a few scenarios where one might like to use a thread to ensure thread-safety of some object (e.g. "All accesses to the private data of X object must be performed on thread Y"), however threads themselves are often too resource-intensive to be able to do that at a smaller scale (e.g. where you have thousands of such objects.)
2525

@@ -32,17 +32,65 @@ Serial queues offer a very interesting option in the "middle" of these two space
3232

3333
# Example
3434

35-
var q = new SerialQueue();
36-
q.DispatchAsync(() => {
37-
Console.WriteLine("a");
38-
});
39-
q.DispatchAsync(() => {
40-
Console.WriteLine("b");
41-
});
35+
```csharp
36+
var q = new SerialQueue();
37+
q.DispatchAsync(() => {
38+
Console.WriteLine("a");
39+
});
40+
q.DispatchAsync(() => {
41+
Console.WriteLine("b");
42+
});
43+
```
4244

4345
In the above example, both operations are guaranteed to execute in-order and guaranteed not to execute at the same time.
4446
Thus, the actions are thread-safe and easy to reason about.
4547

4648
The actual execution of the functions is managed by the built-in .NET ThreadPool (the default implementation just uses `Task.Run`) so many thousands of queues will be backed by perhaps 8 or so underlying OS threads in the threadpool
4749

50+
## Enhancements for .NET
51+
52+
This serial queue supports async/await - i.e. if you are running within the context of a serial queue, then it will be captured across Async/Await
53+
54+
```csharp
55+
var q = new SerialQueue();
56+
q.DispatchAsync(async () => {
57+
// we are on the queue
58+
59+
var response = await SomeNetworkRequest();
60+
61+
// we are still on the queue, it was captured by the await
62+
});
63+
```
64+
65+
You can also await the queue itself directly in order to "jump" to it if you are in an existing async method
66+
67+
```csharp
68+
SerialQueue m_queue = new SerialQueue();
69+
70+
// imagine this is a WPF or winforms app
71+
public void Button_Click()
72+
{
73+
// here we are on the UI main thread
74+
var result = await DoBackgroundProcessing();
75+
76+
// and we are still on the UI thread because 'await DoBackgroundProcessing' captured the sync context.
77+
MyTextBox.Text = result;
78+
}
79+
80+
private async Task<string> DoBackgroundProcessing()
81+
{
82+
// at this point we are still on the UI main thread
83+
84+
await m_queue;
85+
86+
// now we are OFF the main UI thread and onto the serial queue (behind the scenes we're on a threadpool thread)
87+
88+
var response = await SendNetworkRequest();
89+
90+
// still on the serial queue
91+
92+
return response;
93+
}
94+
```
95+
4896
More Documentation is available on the [Github wiki](https://github.com/borland/SerialQueue/wiki)

src/Interfaces.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
1919
// THE SOFTWARE.
2020
using System;
21+
using System.Runtime.CompilerServices;
2122
using System.Threading.Tasks;
2223

2324
#nullable enable
@@ -50,6 +51,9 @@ public interface IDispatchQueue : IDisposable
5051
/// <summary>Checks whether the currently-executing function is
5152
/// on this queue, and throw an OperationInvalidException if it is not</summary>
5253
void VerifyQueue();
54+
55+
/// <summary>Returns the display-name of the queue (if one is set) for diagnostic purposes</summary>
56+
string? Name { get; }
5357
}
5458

5559
/// <summary>A serial queue needs a threadpool to run tasks on. You can provide your own implementation if you want to have a custom threadpool with it's own limits (e.g. no more than X concurrent threads)</summary>
@@ -103,5 +107,29 @@ public static Task<T> DispatchAsync<T>(this IDispatchQueue queue, Func<T> func)
103107
});
104108
return tcs.Task;
105109
}
110+
111+
/// <summary>
112+
/// 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.
113+
/// </summary>
114+
/// <param name="queue">The queue to jump to with your await statement</param>
115+
public static DispatchQueueAwaiter GetAwaiter(this IDispatchQueue queue) => new DispatchQueueAwaiter(queue);
116+
117+
public struct DispatchQueueAwaiter : INotifyCompletion
118+
{
119+
readonly IDispatchQueue m_queue;
120+
121+
public DispatchQueueAwaiter(IDispatchQueue queue)
122+
{
123+
m_queue = queue;
124+
IsCompleted = false;
125+
}
126+
127+
public bool IsCompleted { get; }
128+
129+
public void OnCompleted(Action continuation) => m_queue.DispatchAsync(continuation); // can't cancel here
130+
131+
public void GetResult() { }
132+
133+
}
106134
}
107135
}

src/SerialQueue.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ enum AsyncState
3838
Processing
3939
}
4040

41-
static readonly ThreadLocal<Stack<IDispatchQueue>> s_queueStack = new ThreadLocal<Stack<IDispatchQueue>>(
42-
valueFactory: () => new Stack<IDispatchQueue>(),
41+
static readonly ThreadLocal<Stack<SerialQueue>> s_queueStack = new ThreadLocal<Stack<SerialQueue>>(
42+
valueFactory: () => new Stack<SerialQueue>(),
4343
trackAllValues: false);
4444

4545
readonly IThreadPool m_threadPool;
@@ -56,17 +56,21 @@ enum AsyncState
5656

5757
/// <summary>Constructs a new SerialQueue backed by the given ThreadPool</summary>
5858
/// <param name="threadpool">The threadpool to queue async actions to</param>
59-
public SerialQueue(IThreadPool threadpool, SerialQueueFeatures features)
59+
public SerialQueue(IThreadPool threadpool, string? name = null, SerialQueueFeatures features = SerialQueueFeatures.All)
6060
{
6161
m_threadPool = threadpool ?? throw new ArgumentNullException(nameof(threadpool));
62+
Name = name;
6263
Features = features;
6364

6465
if (features.HasFlag(SerialQueueFeatures.SynchronizationContext))
6566
m_syncContext = new DispatchQueueSynchronizationContext(this);
6667
}
6768

6869
/// <summary>Constructs a new SerialQueue backed by the default TaskThreadPool</summary>
69-
public SerialQueue(SerialQueueFeatures features = SerialQueueFeatures.All) : this(TaskThreadPool.Default, features) { }
70+
public SerialQueue(string? name = null, SerialQueueFeatures features = SerialQueueFeatures.All) : this(TaskThreadPool.Default, name, features) { }
71+
72+
/// <summary>Returns the name (if one is set)</summary>
73+
public string? Name { get; }
7074

7175
/// <summary>Returns the enabled features this serial queue has</summary>
7276
public SerialQueueFeatures Features { get; }
@@ -75,11 +79,15 @@ public SerialQueue(SerialQueueFeatures features = SerialQueueFeatures.All) : thi
7579
/// throws an unhandled exception</summary>
7680
public event EventHandler<UnhandledExceptionEventArgs>? UnhandledException;
7781

82+
/// <summary>Returns the topmost queue that we are currently executing on, or null if we're not on any queue.
83+
/// Note this only works for serial queues specifically, it doesn't generalize to any IDispatchQueue</summary>
84+
public static SerialQueue? Current => s_queueStack.Value.Count > 0 ? s_queueStack.Value.Peek() : null;
85+
7886
/// <summary>Checks whether the currently-executing function is
7987
/// on this queue, and throw an OperationInvalidException if it is not</summary>
8088
public void VerifyQueue()
8189
{
82-
if (s_queueStack == null || !s_queueStack.Value.Contains(this))
90+
if (!s_queueStack.Value.Contains(this))
8391
throw new InvalidOperationException("On the wrong queue");
8492
}
8593

src/SerialQueue.csproj

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,24 @@
22

33
<PropertyGroup>
44
<TargetFrameworks>netstandard2.0;net462</TargetFrameworks>
5-
<AssemblyVersion>2.0.0.0</AssemblyVersion>
6-
<FileVersion>2.0.0.0</FileVersion>
5+
<AssemblyVersion>2.1.0.0</AssemblyVersion>
6+
<FileVersion>2.1.0.0</FileVersion>
77
<LangVersion>8.0</LangVersion>
8-
<Version>2.0.0</Version>
8+
<Version>2.1.0</Version>
99
<Authors>Orion Edwards</Authors>
1010
<Company />
11-
<PackageLicenseUrl>https://raw.githubusercontent.com/borland/SerialQueue/master/LICENSE</PackageLicenseUrl>
11+
<PackageLicenseUrl></PackageLicenseUrl>
1212
<PackageProjectUrl>https://github.com/borland/SerialQueue</PackageProjectUrl>
1313
<Description>C# Implementation of a SerialQueue in the style of Apple's Grand Central Dispatch queues.</Description>
1414
<Copyright>Orion Edwards 2020</Copyright>
15+
<PackageLicenseFile>LICENSE</PackageLicenseFile>
1516
</PropertyGroup>
1617

18+
<ItemGroup>
19+
<None Include="..\LICENSE">
20+
<Pack>True</Pack>
21+
<PackagePath></PackagePath>
22+
</None>
23+
</ItemGroup>
24+
1725
</Project>

tests/SerialQueueSyncContextTest.cs

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public class SerialQueueSyncContextTest
3030
[TestMethod]
3131
public void ChainOfAwaitsFollowsTheQueue()
3232
{
33-
var q = new SerialQueue(SerialQueueFeatures.SynchronizationContext);
33+
var q = new SerialQueue(null, SerialQueueFeatures.SynchronizationContext);
3434

3535
var hits = new List<int>();
3636

@@ -58,7 +58,7 @@ public void ChainOfAwaitsFollowsTheQueue()
5858
[TestMethod]
5959
public void ChainOfAwaitsDoesNotFollowQueueIfFeatureIsOff()
6060
{
61-
var q = new SerialQueue(SerialQueueFeatures.None);
61+
var q = new SerialQueue(null, SerialQueueFeatures.None);
6262

6363
var hits = new List<int>();
6464

@@ -75,7 +75,8 @@ public void ChainOfAwaitsDoesNotFollowQueueIfFeatureIsOff()
7575
q.VerifyQueue();
7676
hits.Add(2);
7777

78-
} catch(InvalidOperationException e) when(e.Message == "On the wrong queue")
78+
}
79+
catch (InvalidOperationException e) when (e.Message == "On the wrong queue")
7980
{
8081
hits.Add(99); // indicate this blew up
8182
}
@@ -86,4 +87,57 @@ public void ChainOfAwaitsDoesNotFollowQueueIfFeatureIsOff()
8687
CollectionAssert.AreEqual(new[] { 1, 99 }, hits);
8788
}
8889
}
90+
91+
[TestClass]
92+
public class SerialQueueAwaiterTest
93+
{
94+
[TestMethod]
95+
public async Task CanAwaitTheQueueItself()
96+
{
97+
var q = new SerialQueue("q1", SerialQueueFeatures.SynchronizationContext);
98+
99+
Assert.IsNull(SerialQueue.Current);
100+
101+
await q;
102+
103+
Assert.AreSame(q, SerialQueue.Current);
104+
}
105+
106+
[TestMethod]
107+
public async Task CanAwaitAcrossManyQueues()
108+
{
109+
var q1 = new SerialQueue("q1", SerialQueueFeatures.SynchronizationContext);
110+
var q2 = new SerialQueue("q2", SerialQueueFeatures.SynchronizationContext);
111+
var q3 = new SerialQueue("q3", SerialQueueFeatures.SynchronizationContext);
112+
113+
Assert.IsNull(SerialQueue.Current);
114+
115+
await q1;
116+
117+
Assert.AreSame(q1, SerialQueue.Current);
118+
119+
await q2;
120+
121+
Assert.AreSame(q2, SerialQueue.Current);
122+
123+
await q3;
124+
125+
Assert.AreSame(q3, SerialQueue.Current);
126+
}
127+
128+
[TestMethod]
129+
public async Task AwaitWorksEvenIfExplicitSyncContextIsSet()
130+
{
131+
var qFalse = new SerialQueue("q1", SerialQueueFeatures.SynchronizationContext);
132+
var q = new SerialQueue("q2", SerialQueueFeatures.SynchronizationContext);
133+
134+
Assert.IsNull(SerialQueue.Current);
135+
136+
SynchronizationContext.SetSynchronizationContext(new DispatchQueueSynchronizationContext(qFalse));
137+
138+
await q;
139+
140+
Assert.AreSame(q, SerialQueue.Current);
141+
}
142+
}
89143
}

0 commit comments

Comments
 (0)