├── Tests
├── Usings.cs
├── Utils.cs
├── Tests.csproj
├── SerialQueue.cs
└── SerialQueueTasks.cs
├── .editorconfig
├── Benchmark
├── Benchmark.csproj
├── SerialQueueTasksSemaphoreSlim.cs
├── SerialQueueMonitor.cs
├── SerialQueueTasksTplDataflow.cs
├── SerialQueueTasksMonitor.cs
├── SerialQueueSpinLock.cs
├── SerialQueueTasksSpinLock.cs
└── Program.cs
├── SerialQueue
├── SerialQueue.csproj
├── SerialQueue.cs
└── SerialQueueTasks.cs
├── LICENSE
├── SerialQueue.sln
├── README.md
└── .gitignore
/Tests/Usings.cs:
--------------------------------------------------------------------------------
1 | global using NUnit.Framework;
2 |
--------------------------------------------------------------------------------
/Tests/Utils.cs:
--------------------------------------------------------------------------------
1 | namespace Tests
2 | {
3 | public static class TestUtils
4 | {
5 | public static Task RandomDelay(int first = 0, int second = 1)
6 | {
7 | return Task.Delay(Random.Shared.Next() % 2 == 0 ? first : second);
8 | }
9 | }
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.cs]
2 |
3 | # CS4014: Because this call is not awaited, execution of the current method continues before the call is completed
4 | dotnet_diagnostic.CS4014.severity = silent
5 |
6 | # Default severity for analyzer diagnostics with category 'Assertion'
7 | dotnet_analyzer_diagnostic.category-Assertion.severity = none
8 |
--------------------------------------------------------------------------------
/Benchmark/Benchmark.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net7.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/Benchmark/SerialQueueTasksSemaphoreSlim.cs:
--------------------------------------------------------------------------------
1 | namespace Threading
2 | {
3 | public class SerialQueueTasksSemaphoreSlim
4 | {
5 | SemaphoreSlim _semaphore = new SemaphoreSlim(1);
6 |
7 | public async Task Enqueue(Action action)
8 | {
9 | await Enqueue(() => {
10 | action();
11 | return true;
12 | });
13 | }
14 |
15 | public async Task Enqueue(Func function)
16 | {
17 | await _semaphore.WaitAsync();
18 | try
19 | {
20 | return function();
21 | }
22 | finally
23 | {
24 | _semaphore.Release();
25 | }
26 | }
27 | }
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/SerialQueue/SerialQueue.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0
5 | enable
6 | enable
7 |
8 |
9 |
10 | 6
11 | 6
12 |
13 |
14 |
15 | TRACE;RELEASE;NET;NET6_0;NETCOREAPP
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/Tests/Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0
5 | enable
6 | enable
7 |
8 | false
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 gentlee (Alexander Danilov)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Tests/SerialQueue.cs:
--------------------------------------------------------------------------------
1 | using Threading;
2 |
3 | namespace Tests
4 | {
5 | [TestFixture]
6 | public class SerialQueueTests
7 | {
8 | [Test]
9 | public void DispatchAsyncFromSingleThread()
10 | {
11 | // Assign
12 |
13 | const int count = 100000;
14 | var queue = new SerialQueue();
15 | var list = new List();
16 | var range = Enumerable.Range(0, count);
17 |
18 | // Act
19 |
20 | foreach (var number in range)
21 | {
22 | queue.DispatchAsync(() => list.Add(number));
23 | }
24 |
25 | queue.DispatchSync(() => { });
26 |
27 | // Assert
28 |
29 | Assert.True(range.SequenceEqual(list));
30 | }
31 |
32 | [Test]
33 | public async Task DispatchAsyncFromMultipleThreads()
34 | {
35 | // Assign
36 |
37 | const int count = 100000;
38 | var counter = -123;
39 | var queue = new SerialQueue();
40 | var list = new List(count);
41 | var tasks = new List(count);
42 |
43 | // Act
44 |
45 | queue.DispatchSync(() =>
46 | {
47 | counter = 0;
48 | });
49 |
50 | for (int i = 0; i < count; i += 1)
51 | {
52 | tasks.Add(Task.Run(() =>
53 | {
54 | queue.DispatchAsync(() =>
55 | {
56 | list.Add(counter);
57 | counter += 1;
58 | });
59 | }));
60 | }
61 |
62 | await Task.WhenAll(tasks);
63 |
64 | queue.DispatchSync(() =>
65 | {
66 | counter *= 2;
67 | });
68 |
69 | // Assert
70 |
71 | Assert.AreEqual(count * 2, counter);
72 | Assert.True(list.SequenceEqual(Enumerable.Range(0, count)));
73 | }
74 | }
75 | }
76 |
77 |
--------------------------------------------------------------------------------
/SerialQueue.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 25.0.1706.8
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SerialQueue", "SerialQueue\SerialQueue.csproj", "{FEF6A814-C39D-4F14-BA6E-8826E0DD0A19}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{14BB4400-87F5-4FDA-8BF1-1D925154796C}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmark", "Benchmark\Benchmark.csproj", "{9611DBBF-0C76-402C-A125-49FCB49920E5}"
11 | EndProject
12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D0276F3C-CEF4-4278-9E81-E19360F73A8A}"
13 | ProjectSection(SolutionItems) = preProject
14 | .editorconfig = .editorconfig
15 | EndProjectSection
16 | EndProject
17 | Global
18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
19 | Debug|Any CPU = Debug|Any CPU
20 | Release|Any CPU = Release|Any CPU
21 | EndGlobalSection
22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
23 | {FEF6A814-C39D-4F14-BA6E-8826E0DD0A19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
24 | {FEF6A814-C39D-4F14-BA6E-8826E0DD0A19}.Debug|Any CPU.Build.0 = Debug|Any CPU
25 | {FEF6A814-C39D-4F14-BA6E-8826E0DD0A19}.Release|Any CPU.ActiveCfg = Release|Any CPU
26 | {FEF6A814-C39D-4F14-BA6E-8826E0DD0A19}.Release|Any CPU.Build.0 = Release|Any CPU
27 | {14BB4400-87F5-4FDA-8BF1-1D925154796C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28 | {14BB4400-87F5-4FDA-8BF1-1D925154796C}.Debug|Any CPU.Build.0 = Debug|Any CPU
29 | {14BB4400-87F5-4FDA-8BF1-1D925154796C}.Release|Any CPU.ActiveCfg = Release|Any CPU
30 | {14BB4400-87F5-4FDA-8BF1-1D925154796C}.Release|Any CPU.Build.0 = Release|Any CPU
31 | {9611DBBF-0C76-402C-A125-49FCB49920E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
32 | {9611DBBF-0C76-402C-A125-49FCB49920E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
33 | {9611DBBF-0C76-402C-A125-49FCB49920E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
34 | {9611DBBF-0C76-402C-A125-49FCB49920E5}.Release|Any CPU.Build.0 = Release|Any CPU
35 | EndGlobalSection
36 | GlobalSection(SolutionProperties) = preSolution
37 | HideSolutionNode = FALSE
38 | EndGlobalSection
39 | GlobalSection(ExtensibilityGlobals) = postSolution
40 | SolutionGuid = {352439E9-A318-4ABF-A835-6CEAB48B5C00}
41 | EndGlobalSection
42 | GlobalSection(MonoDevelopProperties) = preSolution
43 | Policies = $0
44 | $0.TextStylePolicy = $1
45 | $1.FileWidth = 100
46 | $1.TabsToSpaces = True
47 | $1.EolMarker = Unix
48 | $1.scope = text/x-csharp
49 | $0.CSharpFormattingPolicy = $2
50 | $2.scope = text/x-csharp
51 | EndGlobalSection
52 | EndGlobal
53 |
--------------------------------------------------------------------------------
/Benchmark/SerialQueueMonitor.cs:
--------------------------------------------------------------------------------
1 | namespace Threading
2 | {
3 | public class SerialQueueMonitor
4 | {
5 | class LinkedListNode
6 | {
7 | public readonly Action Action;
8 | public LinkedListNode? Next;
9 |
10 | public LinkedListNode(Action action)
11 | {
12 | Action = action;
13 | }
14 | }
15 |
16 | public event Action UnhandledException = delegate { };
17 |
18 | private LinkedListNode? _queueFirst;
19 | private LinkedListNode? _queueLast;
20 | private bool _isRunning = false;
21 |
22 | public void DispatchSync(Action action)
23 | {
24 | var mre = new ManualResetEvent(false);
25 | DispatchAsync(() =>
26 | {
27 | action();
28 | mre.Set();
29 | });
30 | mre.WaitOne();
31 | }
32 |
33 | public void DispatchAsync(Action action)
34 | {
35 | var newNode = new LinkedListNode(action);
36 |
37 | lock (this)
38 | {
39 | if (_queueFirst == null)
40 | {
41 | _queueFirst = newNode;
42 | _queueLast = newNode;
43 |
44 | if (!_isRunning)
45 | {
46 | _isRunning = true;
47 | ThreadPool.QueueUserWorkItem(Run);
48 | }
49 | }
50 | else
51 | {
52 | _queueLast!.Next = newNode;
53 | _queueLast = newNode;
54 | }
55 | }
56 | }
57 |
58 | private void Run(object? _)
59 | {
60 | while (true)
61 | {
62 | LinkedListNode? firstNode;
63 |
64 | lock(this)
65 | {
66 | if (_queueFirst == null)
67 | {
68 | _isRunning = false;
69 | return;
70 | }
71 | firstNode = _queueFirst;
72 | _queueFirst = null;
73 | _queueLast = null;
74 | }
75 |
76 | while (firstNode != null)
77 | {
78 | var action = firstNode.Action;
79 | firstNode = firstNode.Next;
80 | try
81 | {
82 | action();
83 | }
84 | catch (Exception error)
85 | {
86 | UnhandledException.Invoke(action, error);
87 | }
88 | }
89 | }
90 | }
91 | }
92 | }
93 |
94 |
--------------------------------------------------------------------------------
/Benchmark/SerialQueueTasksTplDataflow.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks.Dataflow;
3 |
4 | namespace Threading.Tasks
5 | {
6 | public class SerialQueueTplDataflow
7 | {
8 | ActionBlock