├── CoroutineDispatcher
├── Assembly.cs
├── DispatchPriority.cs
├── DispatcherException.cs
├── CoroutineDispatcher.csproj
├── YieldTask.cs
├── OperationQueue.cs
├── TimerQueue.cs
├── CoroutineSynchronizationContext.cs
└── Dispatcher.cs
├── CoroutineDispatcher.Example
├── CoroutineDispatcher.Example.csproj
├── Producer.cs
├── Consumer.cs
└── Program.cs
├── CoroutineDispatcher.Test
├── CoroutineDispatcher.Test.csproj
├── OperationQueueTest.Fixture.cs
├── TimerQueueTest.Fixture.cs
├── DispatcherTest.Fixture.cs
├── OperationQueueTest.cs
├── TimerQueueTest.cs
└── DispatcherTest.cs
├── LICENSE
├── CoroutineDispatcher.sln
├── .gitattributes
├── .gitignore
└── README.md
/CoroutineDispatcher/Assembly.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | [assembly: InternalsVisibleTo("CoroutineDispatcher.Test")]
4 |
--------------------------------------------------------------------------------
/CoroutineDispatcher/DispatchPriority.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace CoroutineDispatcher
6 | {
7 | public enum DispatchPriority
8 | {
9 | Low,
10 | Medium,
11 | High
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/CoroutineDispatcher/DispatcherException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 |
5 | namespace CoroutineDispatcher
6 | {
7 | public class DispatcherException : Exception
8 | {
9 | public DispatcherException(string message) : base(message)
10 | { }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/CoroutineDispatcher.Example/CoroutineDispatcher.Example.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | netcoreapp3.1
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/CoroutineDispatcher.Test/CoroutineDispatcher.Test.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Tomasz Rewak
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 |
--------------------------------------------------------------------------------
/CoroutineDispatcher/CoroutineDispatcher.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | true
6 | true
7 | MIT
8 | A lightweight framework for running multiple tasks asynchronously on a single thread.
9 | Tomasz Rewak
10 | https://github.com/TomaszRewak/CoroutineDispatcher
11 | https://github.com/TomaszRewak/CoroutineDispatcher
12 | dispatcher, async, threading, coroutines
13 | 1.1.0 - Run methods
14 | 1.0.0 - First release
15 | Tomasz Rewak
16 | Tomasz Rewak
17 | 1.1.0
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/CoroutineDispatcher.Example/Producer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace CoroutineDispatcher.Example
4 | {
5 | internal sealed class Producer
6 | {
7 | private readonly Consumer _consumer;
8 | private readonly Dispatcher _dispatcher;
9 |
10 | private int _counter;
11 |
12 | public Producer(Consumer consumer)
13 | {
14 | _consumer = consumer;
15 | _dispatcher = Dispatcher.Spawn(Initialize);
16 | }
17 |
18 | public void Dispose()
19 | {
20 | _dispatcher.Stop();
21 | }
22 |
23 | public void StartProducing(string item, TimeSpan interval)
24 | {
25 | Log($" Starting production {item}");
26 | _dispatcher.Dispatch(() => Produce(item, interval));
27 | }
28 |
29 | private void Produce(string item, TimeSpan interval)
30 | {
31 | _counter += 1;
32 |
33 | Log($"∩ [start] Producing {item} no {_counter}");
34 |
35 | _consumer.Consume($"{item}_{_counter}");
36 | _dispatcher.Schedule(interval, () => Produce(item, interval));
37 |
38 | Log($"U [end] Producing {item} no {_counter}");
39 | }
40 |
41 | private void Initialize()
42 | {
43 | Log($" Initializing producer");
44 | }
45 |
46 | private static void Log(string text) => Program.Log(2, text);
47 |
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/CoroutineDispatcher.Test/OperationQueueTest.Fixture.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.TestTools.UnitTesting;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Text;
5 |
6 | namespace CoroutineDispatcher.Test
7 | {
8 | public partial class OperationQueueTest
9 | {
10 | private OperationQueue _queue;
11 | private int _dequeuedValue;
12 |
13 | [TestInitialize]
14 | public void Setup()
15 | {
16 | _queue = new OperationQueue();
17 | }
18 |
19 | private void Enqueue(DispatchPriority priority, int value = 0)
20 | {
21 | _queue.Enqueue(priority, () => _dequeuedValue = value);
22 | }
23 |
24 | private void AssertDequeue(int expectedValue)
25 | {
26 | Assert.IsTrue(_queue.TryDequeue(out var operation));
27 | operation();
28 | Assert.AreEqual(expectedValue, _dequeuedValue);
29 | }
30 |
31 | private void AssertFailDequeue()
32 | {
33 | Assert.IsFalse(_queue.TryDequeue(out var operation));
34 | }
35 |
36 | private void AssertCount(int count, DispatchPriority minPriority = DispatchPriority.Low)
37 | {
38 | Assert.AreEqual(count, _queue.Count(minPriority));
39 | }
40 |
41 | private void AssertAny(DispatchPriority minPriority = DispatchPriority.Low)
42 | {
43 | Assert.IsTrue(_queue.Any(minPriority));
44 | }
45 |
46 | private void AssertNone(DispatchPriority minPriority = DispatchPriority.Low)
47 | {
48 | Assert.IsFalse(_queue.Any(minPriority));
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/CoroutineDispatcher/YieldTask.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.CompilerServices;
3 | using System.Threading;
4 |
5 | namespace CoroutineDispatcher
6 | {
7 | public readonly struct YieldTask
8 | {
9 | private readonly DispatchPriority _priority;
10 |
11 | public YieldTask(DispatchPriority priority)
12 | {
13 | _priority = priority;
14 | }
15 |
16 | public YieldTaskAwaiter GetAwaiter()
17 | {
18 | return new YieldTaskAwaiter(_priority);
19 | }
20 | }
21 |
22 | public readonly struct YieldTaskAwaiter : INotifyCompletion
23 | {
24 | private readonly DispatchPriority _priority;
25 |
26 | public YieldTaskAwaiter(DispatchPriority priority)
27 | {
28 | _priority = priority;
29 | }
30 |
31 | public bool IsCompleted
32 | {
33 | get
34 | {
35 | if (!(SynchronizationContext.Current is CoroutineSynchronizationContext context))
36 | throw new DispatcherException("Awaiting Dispatcher.Yield outside of CoroutineSynchronizationContext");
37 |
38 | return !context.HasQueuedTasks(_priority);
39 | }
40 | }
41 |
42 | public void OnCompleted(Action continuation)
43 | {
44 | if (!(SynchronizationContext.Current is CoroutineSynchronizationContext context))
45 | throw new DispatcherException("Awaiting Dispatcher.Yield outside of CoroutineSynchronizationContext");
46 |
47 | context.Post(_priority, continuation);
48 | }
49 |
50 | public void GetResult()
51 | { }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/CoroutineDispatcher/OperationQueue.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace CoroutineDispatcher
5 | {
6 | internal sealed class OperationQueue
7 | {
8 | private readonly object _lock = new object();
9 | private readonly Queue[] _queuedOperations = new Queue[]
10 | {
11 | new Queue(),
12 | new Queue(),
13 | new Queue()
14 | };
15 |
16 | public void Enqueue(DispatchPriority priority, Action operation)
17 | {
18 | lock (_lock)
19 | {
20 | _queuedOperations[(int)priority].Enqueue(operation);
21 | }
22 | }
23 |
24 | public bool TryDequeue(out Action operation)
25 | {
26 | lock (_lock)
27 | {
28 | for (var priority = DispatchPriority.High; priority >= DispatchPriority.Low; --priority)
29 | {
30 | var queue = _queuedOperations[(int)priority];
31 | if (queue.Count > 0)
32 | {
33 | operation = queue.Dequeue();
34 | return true;
35 | }
36 | }
37 | }
38 |
39 | operation = default;
40 | return false;
41 | }
42 |
43 | public int Count(DispatchPriority minPriority)
44 | {
45 | int count = 0;
46 |
47 | lock (_lock)
48 | {
49 | for (var priority = DispatchPriority.High; priority >= minPriority; --priority)
50 | count += _queuedOperations[(int)priority].Count;
51 | }
52 |
53 | return count;
54 | }
55 |
56 | public bool Any(DispatchPriority minPriority)
57 | {
58 | return Count(minPriority) > 0;
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/CoroutineDispatcher/TimerQueue.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace CoroutineDispatcher
5 | {
6 | internal sealed class TimerQueue
7 | {
8 | private readonly object _lock = new object();
9 | private readonly SortedDictionary> _operations = new SortedDictionary>();
10 |
11 | public DateTime? Next
12 | {
13 | get
14 | {
15 | lock (_lock)
16 | {
17 | var enumerator = _operations.GetEnumerator();
18 | return enumerator.MoveNext()
19 | ? enumerator.Current.Key
20 | : (DateTime?)null;
21 | }
22 | }
23 | }
24 |
25 | public void Enqueue(DateTime timestamp, DispatchPriority priority, Action operation)
26 | {
27 | lock (_lock)
28 | {
29 | if (_operations.TryGetValue(timestamp, out var stampedOperations))
30 | stampedOperations.Add((priority, operation));
31 | else
32 | _operations.Add(timestamp, new List<(DispatchPriority, Action)> { (priority, operation) });
33 | }
34 | }
35 |
36 | public bool TryDequeue(out List<(DispatchPriority Priority, Action Operation)> operations)
37 | {
38 | lock (_lock)
39 | {
40 | var enumerator = _operations.GetEnumerator();
41 | if (enumerator.MoveNext() && enumerator.Current.Key <= DateTime.UtcNow)
42 | {
43 | operations = enumerator.Current.Value;
44 | _operations.Remove(enumerator.Current.Key);
45 |
46 | return true;
47 | }
48 | }
49 |
50 | operations = default;
51 | return false;
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/CoroutineDispatcher.Test/TimerQueueTest.Fixture.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.TestTools.UnitTesting;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 |
7 | namespace CoroutineDispatcher.Test
8 | {
9 | public partial class TimerQueueTest
10 | {
11 | private TimerQueue _queue;
12 | private List _dequeuedValues;
13 | private DateTime _now;
14 |
15 | [TestInitialize]
16 | public void Setup()
17 | {
18 | _queue = new TimerQueue();
19 | _dequeuedValues = new List();
20 | _now = DateTime.UtcNow;
21 | }
22 |
23 | private void Enqueue(TimeSpan timeSpan, DispatchPriority priority, Action action)
24 | {
25 | _queue.Enqueue(_now.Add(timeSpan), priority, action);
26 | }
27 |
28 | private void Enqueue(TimeSpan timeSpan, DispatchPriority priority, int value = 0)
29 | {
30 | _queue.Enqueue(_now.Add(timeSpan), priority, () => _dequeuedValues.Add(value));
31 | }
32 |
33 | private void AssertDequeue(int count)
34 | {
35 | Assert.IsTrue(_queue.TryDequeue(out var operations));
36 | Assert.AreEqual(count, operations.Count);
37 | }
38 |
39 | private void AssertFailDequeue()
40 | {
41 | Assert.IsFalse(_queue.TryDequeue(out var _));
42 | }
43 |
44 | private void AssertDequeue(int[] values)
45 | {
46 | Assert.IsTrue(_queue.TryDequeue(out var operations));
47 |
48 | _dequeuedValues.Clear();
49 | foreach (var (_, operation) in operations)
50 | operation();
51 |
52 | CollectionAssert.AreEquivalent(values, _dequeuedValues);
53 | }
54 |
55 | private void AssertNoNext()
56 | {
57 | Assert.IsNull(_queue.Next);
58 | }
59 |
60 | private void AssertNext(TimeSpan timeSpan)
61 | {
62 | Assert.AreEqual(_now.Add(timeSpan), _queue.Next);
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/CoroutineDispatcher.Example/Consumer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 |
6 | namespace CoroutineDispatcher.Example
7 | {
8 | internal sealed class Consumer
9 | {
10 | private readonly Dispatcher _dispatcher = Dispatcher.Spawn();
11 | private readonly HashSet _items = new HashSet();
12 |
13 | public void Dispose()
14 | {
15 | _dispatcher.Stop();
16 | }
17 |
18 | public void Consume(string item) =>
19 | _dispatcher.Dispatch(async () =>
20 | {
21 | Log($" [start] Consuming {item}");
22 |
23 | _items.Add(item);
24 |
25 | if (await CheckOnTheServer(item))
26 | {
27 | await Process(item);
28 | }
29 | else
30 | {
31 | _dispatcher.Dispatch(DispatchPriority.Low, () => Remove(item));
32 | }
33 |
34 | Log($" [end] Consuming {item}");
35 | });
36 |
37 | public int GetCount() =>
38 | _dispatcher.Invoke(() =>
39 | {
40 | Log($"Getting count");
41 | return _items.Count;
42 | });
43 |
44 | private async Task CheckOnTheServer(string item)
45 | {
46 | Log($" [start] Checking on server {item}");
47 | await Task.Delay(TimeSpan.FromSeconds(4));
48 | var result = new Random().Next() % 2 == 0;
49 | Log($" [end] Checking on server {item} ({result})");
50 |
51 | return result;
52 | }
53 |
54 | private async Task Process(string item)
55 | {
56 | Log($"∩ [step 1] Processing {item}");
57 | Thread.Sleep(1000);
58 | Log($"U [step 2] Processing {item}");
59 |
60 | await Dispatcher.Yield(DispatchPriority.Medium);
61 |
62 | Log($"∩ [step 3] Processing {item}");
63 | Thread.Sleep(1000);
64 | Log($"U [step 4] Processing {item}");
65 | }
66 |
67 | private void Remove(string item)
68 | {
69 | Log($" Removing {item}");
70 | }
71 |
72 | private static void Log(string text) => Program.Log(1, text);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/CoroutineDispatcher.Example/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 |
4 | namespace CoroutineDispatcher.Example
5 | {
6 | class Program
7 | {
8 | static void Main()
9 | {
10 | Log("Starting");
11 |
12 | var consumer = new Consumer();
13 | var producer = new Producer(consumer);
14 |
15 | producer.StartProducing("car", TimeSpan.FromSeconds(5));
16 | producer.StartProducing("computer", TimeSpan.FromSeconds(3));
17 |
18 | var mainDispatcher = new Dispatcher();
19 | mainDispatcher.Dispatch(async () =>
20 | {
21 | while (true)
22 | {
23 | var key = Console.ReadKey();
24 |
25 | switch (key.Key)
26 | {
27 | case ConsoleKey.C:
28 | Log("C received");
29 | Dispatcher.Current.Dispatch(DispatchPriority.High, () =>
30 | {
31 | Log($"Counting");
32 | Log($"Count = {consumer.GetCount()}");
33 | });
34 | Log("C dispatcher");
35 | break;
36 | case ConsoleKey.P:
37 | Log("P received");
38 | Dispatcher.Current.Dispatch(DispatchPriority.Low, () =>
39 | {
40 | Log($"Ping");
41 | });
42 | Log("P dispatcher");
43 | break;
44 | case ConsoleKey.E:
45 | Log("E pressed");
46 | Dispatcher.Current.Stop();
47 | return;
48 | }
49 |
50 | await Dispatcher.Yield(DispatchPriority.Medium);
51 | }
52 | });
53 |
54 | Log("Starting main dispatcher");
55 | mainDispatcher.Start();
56 | Log("Stopped main dispatcher");
57 |
58 | Log("Flushing what's left in the main dispatcher");
59 |
60 | mainDispatcher.Execute();
61 |
62 | Log("Stopping consumers");
63 |
64 | producer.Dispose();
65 | consumer.Dispose();
66 | }
67 |
68 | private static void Log(string text) => Log(0, text);
69 | public static void Log(int indent, string text)
70 | {
71 | Console.WriteLine($"{new string('\t', indent * 8)} [thread {Thread.CurrentThread.ManagedThreadId}] {text}");
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/CoroutineDispatcher.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30404.54
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoroutineDispatcher", "CoroutineDispatcher\CoroutineDispatcher.csproj", "{8E42B59B-41E1-4F19-9444-4DD35707575A}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoroutineDispatcher.Example", "CoroutineDispatcher.Example\CoroutineDispatcher.Example.csproj", "{D058DC40-3049-4713-89D6-8E1F9A0AA57A}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoroutineDispatcher.Test", "CoroutineDispatcher.Test\CoroutineDispatcher.Test.csproj", "{1D06E1B0-6F72-4337-A2AC-48AF77A876EA}"
11 | EndProject
12 | Global
13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
14 | Debug|Any CPU = Debug|Any CPU
15 | Release|Any CPU = Release|Any CPU
16 | EndGlobalSection
17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
18 | {8E42B59B-41E1-4F19-9444-4DD35707575A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
19 | {8E42B59B-41E1-4F19-9444-4DD35707575A}.Debug|Any CPU.Build.0 = Debug|Any CPU
20 | {8E42B59B-41E1-4F19-9444-4DD35707575A}.Release|Any CPU.ActiveCfg = Release|Any CPU
21 | {8E42B59B-41E1-4F19-9444-4DD35707575A}.Release|Any CPU.Build.0 = Release|Any CPU
22 | {D058DC40-3049-4713-89D6-8E1F9A0AA57A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {D058DC40-3049-4713-89D6-8E1F9A0AA57A}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {D058DC40-3049-4713-89D6-8E1F9A0AA57A}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {D058DC40-3049-4713-89D6-8E1F9A0AA57A}.Release|Any CPU.Build.0 = Release|Any CPU
26 | {1D06E1B0-6F72-4337-A2AC-48AF77A876EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {1D06E1B0-6F72-4337-A2AC-48AF77A876EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {1D06E1B0-6F72-4337-A2AC-48AF77A876EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {1D06E1B0-6F72-4337-A2AC-48AF77A876EA}.Release|Any CPU.Build.0 = Release|Any CPU
30 | EndGlobalSection
31 | GlobalSection(SolutionProperties) = preSolution
32 | HideSolutionNode = FALSE
33 | EndGlobalSection
34 | GlobalSection(ExtensibilityGlobals) = postSolution
35 | SolutionGuid = {69669BAE-D9A5-4D08-9AB7-0BBFAFB2DD2B}
36 | EndGlobalSection
37 | EndGlobal
38 |
--------------------------------------------------------------------------------
/CoroutineDispatcher.Test/DispatcherTest.Fixture.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.TestTools.UnitTesting;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.Text;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 |
9 | namespace CoroutineDispatcher.Test
10 | {
11 | public partial class DispatcherTest
12 | {
13 | private Dispatcher _dispatcher;
14 | private Thread _mainThread;
15 | private int _callNo;
16 |
17 | [TestInitialize]
18 | public void Setup()
19 | {
20 | _dispatcher = new Dispatcher();
21 | _mainThread = Thread.CurrentThread;
22 | }
23 |
24 | private void Dispatch(Action action) => _dispatcher.Dispatch(action);
25 | private void Dispatch(DispatchPriority priority, Action action) => _dispatcher.Dispatch(priority, action);
26 | private void Dispatch(Func action) => _dispatcher.Dispatch(action);
27 | private void Dispatch(DispatchPriority priority, Func action) => _dispatcher.Dispatch(priority, action);
28 | private void Schedule(TimeSpan timeSpan, Action action) => _dispatcher.Schedule(timeSpan, action);
29 | private void Schedule(TimeSpan timeSpan, DispatchPriority priority, Action action) => _dispatcher.Schedule(timeSpan, priority, action);
30 | private void Invoke(Action action) => _dispatcher.Invoke(action);
31 | private void Invoke(DispatchPriority priority, Action action) => _dispatcher.Invoke(priority, action);
32 | private T Invoke(Func func) => _dispatcher.Invoke(func);
33 | private T Invoke(DispatchPriority priority, Func func) => _dispatcher.Invoke(priority, func);
34 | private Task InvokeAsync(Func> func) => _dispatcher.Invoke(func);
35 |
36 | private void Execute() => _dispatcher.Execute();
37 | private void Start() => _dispatcher.Start();
38 | private void Stop() => _dispatcher.Stop();
39 |
40 | private void AssertCall(int order)
41 | {
42 | Trace.WriteLine($"Main thread call: {order}");
43 | Assert.AreEqual(order, Interlocked.Increment(ref _callNo));
44 | Assert.AreEqual(_mainThread, Thread.CurrentThread);
45 | }
46 |
47 | private void AssertSecondThreadCall(int order)
48 | {
49 | Trace.WriteLine($"Second thread call: {order}");
50 | Assert.AreEqual(order, Interlocked.Increment(ref _callNo));
51 | Assert.AreNotEqual(_mainThread, Thread.CurrentThread);
52 | }
53 |
54 | private void AssertTotalCalls(int calls)
55 | {
56 | Assert.AreEqual(calls, _callNo);
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/CoroutineDispatcher.Test/OperationQueueTest.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.TestTools.UnitTesting;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Text;
5 |
6 | namespace CoroutineDispatcher.Test
7 | {
8 | [TestClass]
9 | public partial class OperationQueueTest
10 | {
11 | [TestMethod]
12 | public void NewQueueHasNoElements()
13 | {
14 | AssertCount(0);
15 | AssertNone();
16 | }
17 |
18 | [TestMethod]
19 | public void EmptyQueueCannotBeDequeued()
20 | {
21 | AssertFailDequeue();
22 | }
23 |
24 | [TestMethod]
25 | public void QueuedOperationsAreCountedIn()
26 | {
27 | Enqueue(DispatchPriority.Medium);
28 | Enqueue(DispatchPriority.Medium);
29 |
30 | AssertCount(2);
31 | AssertAny();
32 | }
33 |
34 | [TestMethod]
35 | public void QueueCountsOnlyOperationsAboveMinimumPriority()
36 | {
37 | Enqueue(DispatchPriority.Low);
38 | Enqueue(DispatchPriority.Low);
39 | Enqueue(DispatchPriority.Medium);
40 | Enqueue(DispatchPriority.Medium);
41 | Enqueue(DispatchPriority.Medium);
42 | Enqueue(DispatchPriority.High);
43 |
44 | AssertCount(1, DispatchPriority.High);
45 | AssertCount(4, DispatchPriority.Medium);
46 | AssertCount(6, DispatchPriority.Low);
47 | }
48 |
49 | [TestMethod]
50 | public void OperateionsAreDequeuedAccordingToThePriority()
51 | {
52 | Enqueue(DispatchPriority.High, 0);
53 | Enqueue(DispatchPriority.Low, 1);
54 | Enqueue(DispatchPriority.Medium, 2);
55 |
56 | AssertDequeue(0);
57 | AssertDequeue(2);
58 | AssertDequeue(1);
59 | }
60 |
61 | [TestMethod]
62 | public void DequeuedOperationsAreRemovedFromTheQueue()
63 | {
64 | Enqueue(DispatchPriority.Medium, 0);
65 | Enqueue(DispatchPriority.Medium, 1);
66 |
67 | AssertDequeue(0);
68 |
69 | AssertCount(1);
70 | }
71 |
72 | [TestMethod]
73 | public void FullyDequeuedQueueIsEmpty()
74 | {
75 | Enqueue(DispatchPriority.Medium, 0);
76 | Enqueue(DispatchPriority.Medium, 1);
77 |
78 | AssertDequeue(0);
79 | AssertDequeue(1);
80 |
81 | AssertNone();
82 | }
83 |
84 | [TestMethod]
85 | public void QueueUsesStableSortingWithinSinglePriority()
86 | {
87 | Enqueue(DispatchPriority.Low, 0);
88 | Enqueue(DispatchPriority.Medium, 1);
89 | Enqueue(DispatchPriority.Medium, 2);
90 | Enqueue(DispatchPriority.High, 3);
91 | Enqueue(DispatchPriority.Medium, 4);
92 | Enqueue(DispatchPriority.High, 5);
93 | Enqueue(DispatchPriority.Medium, 6);
94 | Enqueue(DispatchPriority.Low, 7);
95 |
96 | AssertDequeue(3);
97 | AssertDequeue(5);
98 |
99 | AssertDequeue(1);
100 | AssertDequeue(2);
101 | AssertDequeue(4);
102 | AssertDequeue(6);
103 |
104 | AssertDequeue(0);
105 | AssertDequeue(7);
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/CoroutineDispatcher/CoroutineSynchronizationContext.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 |
7 | namespace CoroutineDispatcher
8 | {
9 | internal sealed class CoroutineSynchronizationContext : SynchronizationContext
10 | {
11 | private readonly OperationQueue _operationQueue = new OperationQueue();
12 | private readonly TimerQueue _timerQueue = new TimerQueue();
13 |
14 | private bool _running;
15 | private CancellationTokenSource _waitToken;
16 |
17 | internal void Start()
18 | {
19 | _running = true;
20 |
21 | while (_running)
22 | {
23 | ExecuteAvailableOperations();
24 | WaitForPendingOperations();
25 | }
26 | }
27 |
28 | internal void Execute()
29 | {
30 | _running = true;
31 | ExecuteAvailableOperations();
32 | }
33 |
34 | internal void Stop()
35 | {
36 | _running = false;
37 | _waitToken?.Cancel();
38 | }
39 |
40 | private void ExecuteAvailableOperations()
41 | {
42 | FlushTimerQueue();
43 |
44 | while (_running && _operationQueue.TryDequeue(out var operation))
45 | {
46 | operation();
47 | FlushTimerQueue();
48 | }
49 | }
50 |
51 | private void WaitForPendingOperations()
52 | {
53 | if (!_running)
54 | return;
55 |
56 | _waitToken = new CancellationTokenSource();
57 |
58 | if (_timerQueue.Next is DateTime next)
59 | _waitToken.CancelAfter(next - DateTime.UtcNow);
60 |
61 | _waitToken.Token.WaitHandle.WaitOne();
62 | _waitToken = null;
63 | }
64 |
65 | public override void Send(SendOrPostCallback d, object state)
66 | {
67 | Send(DispatchPriority.High, () => d(state)).Wait();
68 | }
69 |
70 | internal Task Send(DispatchPriority priority, Action operation)
71 | {
72 | var task = new Task(operation);
73 | Post(priority, () => task.RunSynchronously());
74 | return task;
75 | }
76 |
77 | internal Task Send(DispatchPriority priority, Func operation)
78 | {
79 | var task = new Task(operation);
80 | Post(priority, () => task.RunSynchronously());
81 | return task;
82 | }
83 |
84 | public override void Post(SendOrPostCallback d, object state)
85 | {
86 | Post(DispatchPriority.High, () => d(state));
87 | }
88 |
89 | internal void Post(DispatchPriority priority, Action operation)
90 | {
91 | _operationQueue.Enqueue(priority, operation);
92 | _waitToken?.Cancel();
93 | }
94 |
95 | internal void PostDelayed(DateTime dateTime, DispatchPriority priority, Action action)
96 | {
97 | _timerQueue.Enqueue(dateTime, priority, action);
98 | _waitToken?.Cancel();
99 | }
100 |
101 | internal bool HasQueuedTasks(DispatchPriority priority)
102 | {
103 | return _operationQueue.Any(priority);
104 | }
105 |
106 | private void FlushTimerQueue()
107 | {
108 | while (_timerQueue.TryDequeue(out var operations))
109 | foreach (var (priority, operation) in operations)
110 | Post(priority, operation);
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/CoroutineDispatcher.Test/TimerQueueTest.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.TestTools.UnitTesting;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Text;
5 |
6 | namespace CoroutineDispatcher.Test
7 | {
8 | [TestClass]
9 | public partial class TimerQueueTest
10 | {
11 | [TestMethod]
12 | public void EmptyCollectionHasNothingToDequeue()
13 | {
14 | AssertFailDequeue();
15 | }
16 |
17 | [TestMethod]
18 | public void EnqueueingSingleOperationTwiceStoresItTwice()
19 | {
20 | Action operation = () => { };
21 |
22 | Enqueue(TimeSpan.FromSeconds(-1), DispatchPriority.Medium, operation);
23 | Enqueue(TimeSpan.FromSeconds(-1), DispatchPriority.Medium, operation);
24 |
25 | AssertDequeue(2);
26 | }
27 |
28 | [TestMethod]
29 | public void DequeuingOperationsRemovesThem()
30 | {
31 | Enqueue(TimeSpan.FromSeconds(-1), DispatchPriority.Medium);
32 | Enqueue(TimeSpan.FromSeconds(-1), DispatchPriority.Medium);
33 |
34 | AssertDequeue(2);
35 | AssertFailDequeue();
36 | }
37 |
38 | [TestMethod]
39 | public void OnlyPastOperationsAreDequeued()
40 | {
41 | Enqueue(TimeSpan.FromSeconds(-1), DispatchPriority.Medium, 1);
42 | Enqueue(TimeSpan.FromSeconds(-1), DispatchPriority.Medium, 2);
43 | Enqueue(TimeSpan.FromSeconds(1), DispatchPriority.Medium, 3);
44 | Enqueue(TimeSpan.FromSeconds(1), DispatchPriority.Medium, 4);
45 |
46 | AssertDequeue(new[] { 1, 2 });
47 | AssertFailDequeue();
48 | }
49 |
50 | [TestMethod]
51 | public void DequeuingIsNotImpactedByEnqueuingOrder()
52 | {
53 | Enqueue(TimeSpan.FromSeconds(-1), DispatchPriority.Medium, 1);
54 | Enqueue(TimeSpan.FromSeconds(1), DispatchPriority.Medium, 2);
55 | Enqueue(TimeSpan.FromSeconds(1), DispatchPriority.Medium, 3);
56 | Enqueue(TimeSpan.FromSeconds(-1), DispatchPriority.Medium, 4);
57 |
58 | AssertDequeue(new[] { 1, 4 });
59 | }
60 |
61 | [TestMethod]
62 | public void OnlyTheOldestOperatinosAreDequeuedInASingleStep()
63 | {
64 | Enqueue(TimeSpan.FromSeconds(-2), DispatchPriority.Medium, 1);
65 | Enqueue(TimeSpan.FromSeconds(-1), DispatchPriority.Medium, 2);
66 | Enqueue(TimeSpan.FromSeconds(-1), DispatchPriority.Medium, 3);
67 | Enqueue(TimeSpan.FromSeconds(1), DispatchPriority.Medium, 4);
68 |
69 | AssertDequeue(new[] { 1 });
70 | AssertDequeue(new[] { 2, 3 });
71 | AssertFailDequeue();
72 | }
73 |
74 | [TestMethod]
75 | public void EmptyQueueHasNoNextDate()
76 | {
77 | AssertNoNext();
78 | }
79 |
80 | [TestMethod]
81 | public void NextReturnsTheEarliestDate()
82 | {
83 | Enqueue(TimeSpan.FromSeconds(-1), DispatchPriority.Medium);
84 | Enqueue(TimeSpan.FromSeconds(-4), DispatchPriority.Medium);
85 | Enqueue(TimeSpan.FromSeconds(-2), DispatchPriority.Medium);
86 | Enqueue(TimeSpan.FromSeconds(6), DispatchPriority.Medium);
87 |
88 | AssertNext(TimeSpan.FromSeconds(-4));
89 | }
90 |
91 | [TestMethod]
92 | public void NextIsUpdatedAfterDequeuing()
93 | {
94 | Enqueue(TimeSpan.FromSeconds(-4), DispatchPriority.Medium);
95 | Enqueue(TimeSpan.FromSeconds(-4), DispatchPriority.Medium);
96 | Enqueue(TimeSpan.FromSeconds(3), DispatchPriority.Medium);
97 | Enqueue(TimeSpan.FromSeconds(7), DispatchPriority.Medium);
98 |
99 | AssertDequeue(2);
100 |
101 | AssertNext(TimeSpan.FromSeconds(3));
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Build results
17 | [Dd]ebug/
18 | [Dd]ebugPublic/
19 | [Rr]elease/
20 | [Rr]eleases/
21 | x64/
22 | x86/
23 | [Aa][Rr][Mm]/
24 | [Aa][Rr][Mm]64/
25 | bld/
26 | [Bb]in/
27 | [Oo]bj/
28 | [Ll]og/
29 |
30 | # Visual Studio 2015/2017 cache/options directory
31 | .vs/
32 | # Uncomment if you have tasks that create the project's static files in wwwroot
33 | #wwwroot/
34 |
35 | # Visual Studio 2017 auto generated files
36 | Generated\ Files/
37 |
38 | # MSTest test Results
39 | [Tt]est[Rr]esult*/
40 | [Bb]uild[Ll]og.*
41 |
42 | # NUNIT
43 | *.VisualState.xml
44 | TestResult.xml
45 |
46 | # Build Results of an ATL Project
47 | [Dd]ebugPS/
48 | [Rr]eleasePS/
49 | dlldata.c
50 |
51 | # Benchmark Results
52 | BenchmarkDotNet.Artifacts/
53 |
54 | # .NET Core
55 | project.lock.json
56 | project.fragment.lock.json
57 | artifacts/
58 |
59 | # StyleCop
60 | StyleCopReport.xml
61 |
62 | # Files built by Visual Studio
63 | *_i.c
64 | *_p.c
65 | *_h.h
66 | *.ilk
67 | *.meta
68 | *.obj
69 | *.iobj
70 | *.pch
71 | *.pdb
72 | *.ipdb
73 | *.pgc
74 | *.pgd
75 | *.rsp
76 | *.sbr
77 | *.tlb
78 | *.tli
79 | *.tlh
80 | *.tmp
81 | *.tmp_proj
82 | *_wpftmp.csproj
83 | *.log
84 | *.vspscc
85 | *.vssscc
86 | .builds
87 | *.pidb
88 | *.svclog
89 | *.scc
90 |
91 | # Chutzpah Test files
92 | _Chutzpah*
93 |
94 | # Visual C++ cache files
95 | ipch/
96 | *.aps
97 | *.ncb
98 | *.opendb
99 | *.opensdf
100 | *.sdf
101 | *.cachefile
102 | *.VC.db
103 | *.VC.VC.opendb
104 |
105 | # Visual Studio profiler
106 | *.psess
107 | *.vsp
108 | *.vspx
109 | *.sap
110 |
111 | # Visual Studio Trace Files
112 | *.e2e
113 |
114 | # TFS 2012 Local Workspace
115 | $tf/
116 |
117 | # Guidance Automation Toolkit
118 | *.gpState
119 |
120 | # ReSharper is a .NET coding add-in
121 | _ReSharper*/
122 | *.[Rr]e[Ss]harper
123 | *.DotSettings.user
124 |
125 | # JustCode is a .NET coding add-in
126 | .JustCode
127 |
128 | # TeamCity is a build add-in
129 | _TeamCity*
130 |
131 | # DotCover is a Code Coverage Tool
132 | *.dotCover
133 |
134 | # AxoCover is a Code Coverage Tool
135 | .axoCover/*
136 | !.axoCover/settings.json
137 |
138 | # Visual Studio code coverage results
139 | *.coverage
140 | *.coveragexml
141 |
142 | # NCrunch
143 | _NCrunch_*
144 | .*crunch*.local.xml
145 | nCrunchTemp_*
146 |
147 | # MightyMoose
148 | *.mm.*
149 | AutoTest.Net/
150 |
151 | # Web workbench (sass)
152 | .sass-cache/
153 |
154 | # Installshield output folder
155 | [Ee]xpress/
156 |
157 | # DocProject is a documentation generator add-in
158 | DocProject/buildhelp/
159 | DocProject/Help/*.HxT
160 | DocProject/Help/*.HxC
161 | DocProject/Help/*.hhc
162 | DocProject/Help/*.hhk
163 | DocProject/Help/*.hhp
164 | DocProject/Help/Html2
165 | DocProject/Help/html
166 |
167 | # Click-Once directory
168 | publish/
169 |
170 | # Publish Web Output
171 | *.[Pp]ublish.xml
172 | *.azurePubxml
173 | # Note: Comment the next line if you want to checkin your web deploy settings,
174 | # but database connection strings (with potential passwords) will be unencrypted
175 | *.pubxml
176 | *.publishproj
177 |
178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
179 | # checkin your Azure Web App publish settings, but sensitive information contained
180 | # in these scripts will be unencrypted
181 | PublishScripts/
182 |
183 | # NuGet Packages
184 | *.nupkg
185 | # The packages folder can be ignored because of Package Restore
186 | **/[Pp]ackages/*
187 | # except build/, which is used as an MSBuild target.
188 | !**/[Pp]ackages/build/
189 | # Uncomment if necessary however generally it will be regenerated when needed
190 | #!**/[Pp]ackages/repositories.config
191 | # NuGet v3's project.json files produces more ignorable files
192 | *.nuget.props
193 | *.nuget.targets
194 |
195 | # Microsoft Azure Build Output
196 | csx/
197 | *.build.csdef
198 |
199 | # Microsoft Azure Emulator
200 | ecf/
201 | rcf/
202 |
203 | # Windows Store app package directories and files
204 | AppPackages/
205 | BundleArtifacts/
206 | Package.StoreAssociation.xml
207 | _pkginfo.txt
208 | *.appx
209 |
210 | # Visual Studio cache files
211 | # files ending in .cache can be ignored
212 | *.[Cc]ache
213 | # but keep track of directories ending in .cache
214 | !?*.[Cc]ache/
215 |
216 | # Others
217 | ClientBin/
218 | ~$*
219 | *~
220 | *.dbmdl
221 | *.dbproj.schemaview
222 | *.jfm
223 | *.pfx
224 | *.publishsettings
225 | orleans.codegen.cs
226 |
227 | # Including strong name files can present a security risk
228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
229 | #*.snk
230 |
231 | # Since there are multiple workflows, uncomment next line to ignore bower_components
232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
233 | #bower_components/
234 |
235 | # RIA/Silverlight projects
236 | Generated_Code/
237 |
238 | # Backup & report files from converting an old project file
239 | # to a newer Visual Studio version. Backup files are not needed,
240 | # because we have git ;-)
241 | _UpgradeReport_Files/
242 | Backup*/
243 | UpgradeLog*.XML
244 | UpgradeLog*.htm
245 | ServiceFabricBackup/
246 | *.rptproj.bak
247 |
248 | # SQL Server files
249 | *.mdf
250 | *.ldf
251 | *.ndf
252 |
253 | # Business Intelligence projects
254 | *.rdl.data
255 | *.bim.layout
256 | *.bim_*.settings
257 | *.rptproj.rsuser
258 | *- Backup*.rdl
259 |
260 | # Microsoft Fakes
261 | FakesAssemblies/
262 |
263 | # GhostDoc plugin setting file
264 | *.GhostDoc.xml
265 |
266 | # Node.js Tools for Visual Studio
267 | .ntvs_analysis.dat
268 | node_modules/
269 |
270 | # Visual Studio 6 build log
271 | *.plg
272 |
273 | # Visual Studio 6 workspace options file
274 | *.opt
275 |
276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
277 | *.vbw
278 |
279 | # Visual Studio LightSwitch build output
280 | **/*.HTMLClient/GeneratedArtifacts
281 | **/*.DesktopClient/GeneratedArtifacts
282 | **/*.DesktopClient/ModelManifest.xml
283 | **/*.Server/GeneratedArtifacts
284 | **/*.Server/ModelManifest.xml
285 | _Pvt_Extensions
286 |
287 | # Paket dependency manager
288 | .paket/paket.exe
289 | paket-files/
290 |
291 | # FAKE - F# Make
292 | .fake/
293 |
294 | # JetBrains Rider
295 | .idea/
296 | *.sln.iml
297 |
298 | # CodeRush personal settings
299 | .cr/personal
300 |
301 | # Python Tools for Visual Studio (PTVS)
302 | __pycache__/
303 | *.pyc
304 |
305 | # Cake - Uncomment if you are using it
306 | # tools/**
307 | # !tools/packages.config
308 |
309 | # Tabs Studio
310 | *.tss
311 |
312 | # Telerik's JustMock configuration file
313 | *.jmconfig
314 |
315 | # BizTalk build output
316 | *.btp.cs
317 | *.btm.cs
318 | *.odx.cs
319 | *.xsd.cs
320 |
321 | # OpenCover UI analysis results
322 | OpenCover/
323 |
324 | # Azure Stream Analytics local run output
325 | ASALocalRun/
326 |
327 | # MSBuild Binary and Structured Log
328 | *.binlog
329 |
330 | # NVidia Nsight GPU debugger configuration file
331 | *.nvuser
332 |
333 | # MFractors (Xamarin productivity tool) working folder
334 | .mfractor/
335 |
336 | # Local History for Visual Studio
337 | .localhistory/
338 |
339 | # BeatPulse healthcheck temp database
340 | healthchecksdb
--------------------------------------------------------------------------------
/CoroutineDispatcher.Test/DispatcherTest.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.TestTools.UnitTesting;
2 | using System;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 |
6 | namespace CoroutineDispatcher.Test
7 | {
8 | [TestClass]
9 | public partial class DispatcherTest
10 | {
11 | [TestMethod]
12 | public void ExecutesQueuedOperations()
13 | {
14 | Dispatch(() => AssertCall(1));
15 | Dispatch(() => AssertCall(2));
16 |
17 | Execute();
18 |
19 | AssertTotalCalls(2);
20 | }
21 |
22 | [TestMethod]
23 | public void ExecutesQueuedOperationsAccordingToPriority()
24 | {
25 | Dispatch(DispatchPriority.Low, () => AssertCall(3));
26 | Dispatch(DispatchPriority.High, () => AssertCall(1));
27 | Dispatch(DispatchPriority.Medium, () => AssertCall(2));
28 |
29 | Execute();
30 |
31 | AssertTotalCalls(3);
32 | }
33 |
34 | [TestMethod]
35 | public void UsesStableSortingForPriorityLevels()
36 | {
37 | Dispatch(DispatchPriority.Low, () => AssertCall(4));
38 | Dispatch(DispatchPriority.High, () => AssertCall(1));
39 | Dispatch(DispatchPriority.Low, () => AssertCall(5));
40 | Dispatch(DispatchPriority.Low, () => AssertCall(6));
41 | Dispatch(DispatchPriority.Medium, () => AssertCall(3));
42 | Dispatch(DispatchPriority.High, () => AssertCall(2));
43 |
44 | Execute();
45 |
46 | AssertTotalCalls(6);
47 | }
48 |
49 | [TestMethod]
50 | public void SetsCurrentDispatcherDuringExecution()
51 | {
52 | Dispatch(() => Assert.AreEqual(_dispatcher, Dispatcher.Current));
53 | Execute();
54 | }
55 |
56 | [TestMethod]
57 | public void ClearsCurrentDispatcherAfterExecution()
58 | {
59 | Execute();
60 | Assert.IsNull(Dispatcher.Current);
61 | }
62 |
63 | [TestMethod]
64 | public void QueuesTasksDispatchedDuringExecution()
65 | {
66 | Dispatch(() =>
67 | {
68 | AssertCall(1);
69 | Dispatch(() => AssertCall(3));
70 | Dispatch(() => AssertCall(4));
71 | AssertCall(2);
72 | });
73 |
74 | Execute();
75 |
76 | AssertTotalCalls(4);
77 | }
78 |
79 | [TestMethod]
80 | public void FinishesCurrentOperationBeforeExecutingHigherPriorityOperations()
81 | {
82 | Dispatch(DispatchPriority.Low, () =>
83 | {
84 | AssertCall(1);
85 | Dispatch(DispatchPriority.High, () => AssertCall(3));
86 | AssertCall(2);
87 | });
88 |
89 | Execute();
90 |
91 | AssertTotalCalls(3);
92 | }
93 |
94 | [TestMethod]
95 | public void YieldsExecutionToHigherPriorityOperations()
96 | {
97 | Dispatch(DispatchPriority.Medium, async () =>
98 | {
99 | AssertCall(1);
100 | Dispatch(DispatchPriority.Medium, () => AssertCall(4));
101 | Dispatch(DispatchPriority.High, () => AssertCall(3));
102 | Dispatch(DispatchPriority.Low, () => AssertCall(6));
103 | AssertCall(2);
104 | await Dispatcher.Yield(DispatchPriority.Medium);
105 | AssertCall(5);
106 | });
107 |
108 | Execute();
109 |
110 | AssertTotalCalls(6);
111 | }
112 |
113 | [TestMethod]
114 | public void SetsSynchronizationContextDuringExecution()
115 | {
116 | Dispatch(() =>
117 | {
118 | Assert.IsInstanceOfType(SynchronizationContext.Current, typeof(CoroutineSynchronizationContext));
119 | });
120 |
121 | Execute();
122 | }
123 |
124 | [TestMethod]
125 | public void RestoresSynchronizationContextAfterExecution()
126 | {
127 | Execute();
128 | Assert.IsNotInstanceOfType(SynchronizationContext.Current, typeof(CoroutineSynchronizationContext));
129 | }
130 |
131 | [TestMethod, ExpectedException(typeof(DispatcherException))]
132 | public async Task YieldingDispatcherOutsideTheExecutionCycleThrowsAnException()
133 | {
134 | await Dispatcher.Yield(DispatchPriority.Medium);
135 | }
136 |
137 | [TestMethod]
138 | public void ExecutionIsContinuedOnTheMainThreadAfterAwaitingSecondThread()
139 | {
140 | Dispatch(async () =>
141 | {
142 | AssertCall(1);
143 | await Task.Run(() => AssertSecondThreadCall(2));
144 | AssertCall(3);
145 | Stop();
146 | });
147 |
148 | Start();
149 |
150 | AssertTotalCalls(3);
151 | }
152 |
153 | [TestMethod]
154 | public void OtherOperationsAreExecutedWhileAwaitingSecondThread()
155 | {
156 | Dispatch(DispatchPriority.High, async () =>
157 | {
158 | Dispatch(DispatchPriority.Low, () => AssertCall(2));
159 | AssertCall(1);
160 | await Task.Run(() => Thread.Sleep(50));
161 | AssertCall(3);
162 | Stop();
163 | });
164 |
165 | Start();
166 |
167 | AssertTotalCalls(3);
168 | }
169 |
170 | [TestMethod]
171 | public void ScheduledOperationsAreNotExecutedBeforeTheTimeout()
172 | {
173 | Schedule(TimeSpan.FromMilliseconds(100), () => AssertCall(3));
174 | Dispatch(async () =>
175 | {
176 | AssertCall(1);
177 | await Task.Delay(50);
178 | AssertCall(2);
179 | await Task.Delay(100);
180 | AssertCall(4);
181 | Stop();
182 | });
183 |
184 | Start();
185 |
186 | AssertTotalCalls(4);
187 | }
188 |
189 | [TestMethod]
190 | public void MultipleScheduledOperatinosAreExecutedAccordingToTheirTimeout()
191 | {
192 | Schedule(TimeSpan.FromMilliseconds(300), () => AssertCall(7));
193 | Schedule(TimeSpan.FromMilliseconds(100), () => AssertCall(3));
194 | Schedule(TimeSpan.FromMilliseconds(200), () => AssertCall(5));
195 | Dispatch(async () =>
196 | {
197 | AssertCall(1);
198 | await Task.Delay(50);
199 | AssertCall(2);
200 | await Task.Delay(100);
201 | AssertCall(4);
202 | await Task.Delay(100);
203 | AssertCall(6);
204 | await Task.Delay(100);
205 | AssertCall(8);
206 | Stop();
207 | });
208 |
209 | Start();
210 |
211 | AssertTotalCalls(8);
212 | }
213 |
214 | [TestMethod]
215 | public void OperationsScheduledInThePastAreQueuedRightAway()
216 | {
217 | Dispatch(DispatchPriority.Medium, () =>
218 | {
219 | AssertCall(1);
220 | Dispatch(DispatchPriority.High, () => AssertCall(3));
221 | Dispatch(DispatchPriority.Low, () => AssertCall(5));
222 | Schedule(TimeSpan.FromSeconds(-1), DispatchPriority.Medium, () => AssertCall(4));
223 | AssertCall(2);
224 | });
225 |
226 | Execute();
227 |
228 | AssertTotalCalls(5);
229 | }
230 |
231 | [TestMethod]
232 | public void StoppingStopsAlsoContinuousExecution()
233 | {
234 | Dispatch(() => AssertCall(1));
235 | Dispatch(() => AssertCall(2));
236 | Dispatch(() =>
237 | {
238 | AssertCall(3);
239 | Stop();
240 | });
241 | Dispatch(() => AssertCall(4));
242 |
243 | Execute();
244 |
245 | AssertTotalCalls(3);
246 | }
247 |
248 | [TestMethod]
249 | public void ScheduledOperationsAreIncludedDuringContinuousExecutionWithCorrectPriority()
250 | {
251 | Schedule(TimeSpan.FromMilliseconds(100), DispatchPriority.Medium, () => throw new InvalidOperationException());
252 | Schedule(TimeSpan.FromMilliseconds(200), DispatchPriority.High, () => Stop());
253 | void Loop() { Dispatch(DispatchPriority.High, Loop); }
254 | Loop();
255 |
256 | Execute();
257 | }
258 |
259 | [TestMethod]
260 | public void InvokingBlockTheCallingThread()
261 | {
262 | Dispatch(() =>
263 | {
264 | AssertCall(1);
265 | Task.Run(() => Invoke(() => AssertSecondThreadCall(3)));
266 | Thread.Sleep(100);
267 | Dispatch(() =>
268 | {
269 | AssertCall(4);
270 | Stop();
271 | });
272 | AssertCall(2);
273 | });
274 |
275 | Start();
276 | AssertTotalCalls(4);
277 | }
278 |
279 | [TestMethod]
280 | public void InvokedOperationCanRetrunAValue()
281 | {
282 | Dispatch(() =>
283 | {
284 | AssertCall(1);
285 | Task.Run(() =>
286 | {
287 | AssertSecondThreadCall(3);
288 | Assert.AreEqual(123, Invoke(() =>
289 | {
290 | AssertCall(4);
291 | return 123;
292 | }));
293 | AssertSecondThreadCall(5);
294 | Stop();
295 | });
296 | AssertCall(2);
297 | });
298 |
299 | Start();
300 | AssertTotalCalls(5);
301 | }
302 |
303 | [TestMethod]
304 | public void AsyncInvokeDoesNotBlockTheCallingThread()
305 | {
306 | var second = Dispatcher.Spawn();
307 |
308 | Dispatch(async () =>
309 | {
310 | Dispatch(() => AssertCall(2));
311 | AssertCall(1);
312 | await second.InvokeAsync(() =>
313 | {
314 | Thread.Sleep(50);
315 | AssertSecondThreadCall(3);
316 | });
317 | AssertCall(4);
318 | Stop();
319 | });
320 |
321 | Start();
322 | AssertTotalCalls(4);
323 | }
324 |
325 | [TestMethod]
326 | public void AwaitCallbackIsDispatchedWithHighPriority()
327 | {
328 | Dispatch(async () =>
329 | {
330 | void Loop() { Dispatch(DispatchPriority.High, Loop); }
331 | Loop();
332 |
333 | await Task.Delay(100);
334 | Stop();
335 | });
336 |
337 | Start();
338 | }
339 |
340 | [TestMethod]
341 | public void InvokingTheSameThreadExecutesTheOperationRightAway()
342 | {
343 | Dispatch(() =>
344 | {
345 | AssertCall(1);
346 | Dispatch(DispatchPriority.High, () => AssertCall(5));
347 | Invoke(DispatchPriority.Low, () => AssertCall(2));
348 | Assert.AreEqual(123, Invoke(DispatchPriority.Low, () =>
349 | {
350 | AssertCall(3);
351 | return 123;
352 | }));
353 | AssertCall(4);
354 | });
355 |
356 | Execute();
357 | AssertTotalCalls(5);
358 | }
359 |
360 | [TestMethod]
361 | public void InvokedOperationCanByAsynchronous()
362 | {
363 | Dispatch(() =>
364 | {
365 | AssertCall(1);
366 | Task.Run(async () =>
367 | {
368 | AssertSecondThreadCall(2);
369 | Assert.AreEqual(123, await InvokeAsync(async () =>
370 | {
371 | AssertCall(3);
372 | Dispatch(() => AssertCall(5));
373 | AssertCall(4);
374 | await Task.Delay(100);
375 | AssertCall(6);
376 | return 123;
377 | }));
378 | AssertSecondThreadCall(7);
379 | Stop();
380 | });
381 | });
382 |
383 | Start();
384 | AssertTotalCalls(7);
385 | }
386 | }
387 | }
388 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CoroutineDispatcher
2 |
3 | CoroutineDispatcher is a lightweight framework for running multiple tasks asynchronously on a single thread.
4 |
5 | ### Why?
6 |
7 | With heavily multi-threaded applications it can be difficult to manage data synchronization and ownership. Performing all related operations on a single thread can help in achieving much more predictable behavior with a code that's much easier to maintain. In some cases it can even result in a better performance.
8 |
9 | Of course it doesn’t mean that one has to be limited to just a single thread. The CoroutineDispatcher can be used to spawn multiple independent task queues that can communicate with each other by dispatching tasks on one another.
10 |
11 | ### Inspiration
12 |
13 | The `CoroutineDispatcher` is strongly inspired by the `System.Windows.Dispatcher` used within the WPF framework. Just instead of using the default Windows message pump, it relays on a basic operation queue that is consumed within a relatively simple execution loop.
14 |
15 | ### What is it?
16 |
17 | The heart of the `CoroutineDispatcher` is the `Dispatcher` class. You can think of it as a fancy priority queue that stores tasks to be run. By starting the `Dispatcher` you initialize an infinite loop that processes queued operations and waits for new ones once the list is empty.
18 |
19 | Tasks can be scheduled beforehand or during execution (both from the thread on which the dispatcher is currently running as well as other threads).
20 |
21 | On top of that simple concept the CoroutineDispatcher provides a handful of useful abstractions that make it easy to use within real world scenarios.
22 |
23 | ### How to use it?
24 |
25 | First: install the package in your project.
26 |
27 | `> dotnet add package CoroutineDispatcher`
28 |
29 | Second: simply create a `Dispatcher`, queue (or not) some operations and start the execution.
30 |
31 | ```csharp
32 | var dispatcher = new Dispatcher();
33 |
34 | void F1() =>
35 | dispatcher.Dispatch(() =>
36 | {
37 | Console.Write(4)
38 | });
39 |
40 | void F2() =>
41 | dispatcher.Dispatch(DispatchPriority.High, () =>
42 | {
43 | Console.Write(3)
44 | });
45 |
46 | void F3() =>
47 | dispatcher.Dispatch(DispatchPriority.Low, () =>
48 | {
49 | Console.Write(5)
50 | });
51 |
52 | void F4() =>
53 | dispatcher.Dispatch(() =>
54 | {
55 | Console.Write(1);
56 | F1();
57 | F2();
58 | Console.Write(2);
59 | });
60 |
61 | F3();
62 | F4();
63 |
64 | dispatcher.Start(); // Prints "12345" and waits for new operations
65 | ```
66 |
67 | The `Dispatch` method is the simples of the task queueing operations. It’s a basic “fire and forget” used when the calling thread is not interested in the result of the task execution.
68 |
69 | An alternative to the `Dispatch` method is the `Invoke` method. It also queues the task on the dispatcher, but stops the calling thread until the execution of that task is over. It allows us not only to synchronize threads, but also easily acquire results of computations.
70 |
71 | ```csharp
72 | var dispatcher = new Dispatcher();
73 |
74 | void Add(int a, int b) =>
75 | dispatcher.Invoke(() => {
76 | return a + b; // This executes on the dispatcher thread along with other operations (a and b are captured in a closure)
77 | });
78 |
79 | Task.Run(() => {
80 | int a = 2, b = 2; // This executes on a separate thread
81 | var sum = Add(a, b);
82 | Console.Write(sum); // And here we are on the background thread again
83 | });
84 |
85 | dispatcher.Start();
86 | ```
87 |
88 | (Please excuse the amount of lambdas used in those examples - it's just easier to create simple and compact snippets this way)
89 |
90 | The `CoroutineDispatcher` (of course) also provides support for dispatching asynchronous operations.
91 |
92 | ```csharp
93 | var dispatcher1 = new Dispatcher();
94 | var dispatcher2 = Dispatcher.Spawn(); // Creates and starts a dispatcher on a new thread
95 |
96 | dispatcher1.Dispatch(async () =>
97 | {
98 | var r = 2.0;
99 | // While we await the result, the dispatcher1 is free to execute other pending operations
100 | var v = await dispatcher2.InvokeAsync(() => Math.PI * r * r);
101 | // The continuation of the awaited operation is properly dispatched back on the dispatcher1
102 | Console.Write(v);
103 | });
104 | dispatcher1.Dispatch(() => { });
105 |
106 | dispatcher1.Start();
107 | ```
108 |
109 | You can even intentionally yield execution back to the dispatcher if you are worried that other tasks might get starved during an execution of a long running operation.
110 |
111 | ```csharp
112 | var dispatcher = new Dispatcher();
113 |
114 | dispatcher.Dispatch(async () =>
115 | {
116 | Console.Write(1);
117 | dispatcher.Dispatch(DispatchPriority.Low, () => Console.Write(4));
118 | dispatcher.Dispatch(DispatchPriority.High, () => Console.Write(2));
119 | Thread.Sleep(1000);
120 |
121 | // Will allow for execution of pending tasks with at least medium priority before continuing
122 | await Dispatcher.Yield(DispatchPriority.Medium);
123 |
124 | Thread.Sleep(1000);
125 | Console.Write(3);
126 | });
127 |
128 | dispatcher.Start(); // Will print "1234"
129 | ```
130 |
131 | The last but not least is the task scheduling. It can be used run a task after a specified delay.
132 |
133 | ```csharp
134 | var dispatcher = new Dispatcher();
135 |
136 | dispatcher.Schedule(TimeSpan.FromSeconds(30), () => Console.Write("At least 30 seconds have passed"));
137 |
138 | dispatcher.Start();
139 | ```
140 |
141 | Once you are done with a Dispatcher you can simply call the `Stop()` method. It will not terminate the currently running task, but will prevent the dispatcher from consuming any new ones. But don’t worry - queued tasks are not lost. Calling `Start()` again will resume the processing from where it was left off.
142 |
143 | And that's it from the most essential basics. Maybe not much - but for many use cases - more then enough.
144 |
145 | At the end, just as a hint, I want to share that I've found the following pattern (also used in some of the above) to be the most handy when working with tasks management in systems with multiple dispatchers.
146 |
147 | ```csharp
148 | internal sealed class Consumer
149 | {
150 | private readonly Dispatcher _dispatcher = Dispatcher.Spawn();
151 |
152 | public void Consume(string item) =>
153 | _dispatcher.Dispatch(async () =>
154 | {
155 | ...
156 | });
157 |
158 | public Task GetCount() =>
159 | _dispatcher.InvokeAsync(() =>
160 | {
161 | ...
162 | });
163 |
164 | private async Task CheckOnTheServer(string item)
165 | {
166 | ...
167 | }
168 | }
169 | ```
170 |
171 | So instead of making it a responsibility of the caller to know where to dispatch an operation - in this pattern we annotate the public interface of a component owning the resources with correct task management operations to make sure that the work is always performed on a correct thread.
172 |
173 | But we can also reduce the first example in this README to just this:
174 |
175 | ```csharp
176 | var dispatcher = new Dispatcher();
177 |
178 | dispatcher.Dispatch(() =>
179 | {
180 | Console.Write(1);
181 | dispatcher.Dispatch(DispatchPriority.High, () => Console.Write(3));
182 | dispatcher.Dispatch(DispatchPriority.Low, () => Console.Write(5));
183 | Console.Write(2);
184 | });
185 | dispatcher.Dispatch(DispatchPriority.Medium, () => Console.Write(4));
186 |
187 | dispatcher.Start(); // Prints "12345" and waits for new operations
188 | ```
189 |
190 | ### The `CoroutineDispatcher.Dispatcher` class
191 |
192 | ##### `Dispatcher Dispatcher.Current { get; }`
193 |
194 | Returns a `Dispatcher` currently running on the calling thread. By using it you don't have to pass around the reference to your dispatcher.
195 |
196 | ##### `void Dispatcher.Start()`
197 |
198 | Starts an infinite loop on the current thread to process all queued and scheduled operations. Once the queue of operations is empty, waits for new ones to arrive.
199 |
200 | ##### `void Dispatcher.Stop()`
201 |
202 | Stops the current execution. If called from within an operation currently executed by the dispatcher, the operation itself will not be terminated, but no new operations will be acquired from the operation queue once the current operation finishes or yields.
203 |
204 | ##### `void Dispatcher.Execute()`
205 |
206 | Processes all queued operations until the active operation queue is empty. Performed execution will include all due scheduled operations, but will not wait for operations scheduled for after the active operation queue has been emptied nor the callbacks of awaited operations that did not finish yet.
207 |
208 | ##### `bool Dispatcher.CheckAccess()`
209 |
210 | Returns `true` if this instance of the `Dispatcher` is currently running on the calling thread.
211 |
212 | ##### `static Dispatcher Dispatcher.Spawn()`
213 |
214 | Creates a new `Dispatcher` and starts it on a new thread.
215 |
216 | ##### `void Dispatcher.Dispatch(...)`
217 | ###### `void Dispatch([DispatchPriority priority = DispatchPriority.Medium,] Action operation)`
218 | ###### `void Dispatch([DispatchPriority priority = DispatchPriority.Medium,] Func operation)`
219 |
220 | Adds the `operation` to the operation queue without blocking of the current thread (fire and forget).
221 |
222 | ```
223 | ═════════ dispatcher2.Dispatch(...) ═════════
224 |
225 | dispatcher1 dispatcher2 thread_pool
226 | │ │ ║
227 | ∩ │ ║
228 | ║ ∩ ║
229 | ╟───────┐ ║ ║
230 | ║ │ U ║
231 | U └────────╖ ║
232 | ∩ ║ ┌────────╢
233 | ║ U │ ║
234 | ║ ∩ │ ║
235 | ║ ┌────╢ │ ║
236 | U │ U │ ║
237 | │ │ ╓───────┘ ║
238 | ∩ │ ║ ║
239 | ║ │ U ║
240 | U └────╖ ║
241 | │ ║ ║
242 | ```
243 |
244 | ##### `... Dispatcher.Invoke(...)`
245 | ###### `void Invoke([DispatchPriority priority = DispatchPriority.Medium,] Action operation)`
246 | ###### `T Invoke([DispatchPriority priority = DispatchPriority.Medium,] Func operation)`
247 |
248 | Adds the `operation` to the operation queue and stops the calling thread until it's executed.
249 |
250 | If called from within an operation currently executed by the `Dispatcher.Current`, the provided `operation` will be performed in place to avoid deadlocks.
251 |
252 | ```
253 | ══════════ dispatcher2.Invoke(...) ══════════
254 |
255 | dispatcher1 dispatcher2 thread_pool
256 | │ │ ║
257 | ∩ │ ║
258 | ║ ∩ ║
259 | ╙───────┐ ║ ║
260 | . │ U ║
261 | . └────────╖ ║
262 | . ║ ┌────────╜
263 | . ║ │ .
264 | ╓────────────────╜ │ .
265 | ║ ∩ │ .
266 | U U │ .
267 | │ ╓───────┘ .
268 | ∩ ║ .
269 | ║ ╙────────────────╖
270 | U │ ║
271 | ```
272 |
273 | ##### `... Dispatcher.InvokeAsync(...)`
274 | ###### `Task InvokeAsync([DispatchPriority priority = DispatchPriority.Medium,] Action operation)`
275 | ###### `Task InvokeAsync([DispatchPriority priority = DispatchPriority.Medium,] Func operation)`
276 | ###### `Task InvokeAsync([DispatchPriority priority = DispatchPriority.Medium,] Func operation)`
277 | ###### `Task InvokeAsync([DispatchPriority priority = DispatchPriority.Medium,] Func> operation)`
278 |
279 | Adds the `operation` to the operation queue and returns a `Task` associated with the state of its execution.
280 |
281 | ```
282 | ═════ await dispatcher2.InvokeAsync(...) ═════
283 |
284 | dispatcher1 dispatcher2 thread_pool
285 | │ │ ║ │
286 | ∩ │ ║ │
287 | ║ ∩ ║ │
288 | ╙───────┐ ║ ║ │
289 | │ │ U ║ │
290 | ∩ └────────╖ ║ │
291 | ║ ║ ┌───────╜ │
292 | ║ ║ │ │ │
293 | ║ ┌────────╜ │ │ │
294 | U │ ∩ │ │ │
295 | ╓───────┘ U │ │ │
296 | ║ ╓───────┘ │ │
297 | U ║ │ │
298 | ∩ ╙───────────────┼ ╖
299 | U │ │ ║
300 | ```
301 |
302 | ##### `void Schedule(...)`
303 | ###### `void Schedule(TimeSpan delay, [DispatchPriority priority = DispatchPriority.Medium,] Action operation)`
304 | ###### `void Schedule(TimeSpan delay, [DispatchPriority priority = DispatchPriority.Medium,] Func operation)`
305 |
306 | Schedules the execution of the `operation` after the provided `delay`.
307 |
308 | It is not guaranteed that the `operation` will be executed exactly after the provided `delay` (it's only guaranteed that it will be queued not sooner then that).
309 |
310 | ##### `static YieldTask Dispatcher.Yield(DispatchPriority priority = DispatchPriority.Low)`
311 |
312 | When awaited, will yield the execution of the current dispatcher to other queued operations with at least given `priority`.
313 |
314 | ```
315 | ════════ await Dispatcher.Yield(...) ════════
316 |
317 | dispatcher
318 | │
319 | │
320 | ∩
321 | ║
322 | ┌────╜
323 | │ ∩
324 | │ ║
325 | │ U
326 | │ ∩
327 | │ ║
328 | │ U
329 | └────╖
330 | ║
331 | U
332 | │
333 | ```
334 |
335 | ##### `void Dispatcher.Run(...)`
336 | ###### `void Run([DispatchPriority priority = DispatchPriority.Medium,] Action operation)`
337 | ###### `void Run([DispatchPriority priority = DispatchPriority.Medium,] Func operation)`
338 |
339 | If called for another thread, works similarly to the `Dispatcher.Dispatch(...)`, but if called from a thread on wich the `Dispatcher.Current` is currently running - executes the `operation` inline.
340 |
341 | # Contributions
342 |
343 | I'm open to tickets and contributions.
344 |
--------------------------------------------------------------------------------
/CoroutineDispatcher/Dispatcher.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 |
5 | namespace CoroutineDispatcher
6 | {
7 | public class Dispatcher
8 | {
9 | [ThreadStatic]
10 | private static Dispatcher _current = null;
11 | ///
12 | /// Returns a currently running on the calling thread.
13 | ///
14 | ///
15 | /// Will return null if there is no running on the calling thread.
16 | ///
17 | public static Dispatcher Current => _current;
18 |
19 | private readonly CoroutineSynchronizationContext _synchronizationContext = new CoroutineSynchronizationContext();
20 |
21 | ///
22 | /// Starts an infinite loop on the current thread to process all queued and scheduled operations.
23 | ///
24 | ///
25 | public void Start()
26 | {
27 | var oldDispatcher = _current;
28 | var oldSynchronizationContext = SynchronizationContext.Current;
29 |
30 | try
31 | {
32 | _current = this;
33 | SynchronizationContext.SetSynchronizationContext(_synchronizationContext);
34 |
35 | _synchronizationContext.Start();
36 | }
37 | finally
38 | {
39 | _current = oldDispatcher;
40 | SynchronizationContext.SetSynchronizationContext(oldSynchronizationContext);
41 | }
42 | }
43 |
44 | ///
45 | /// Processes all queued operations until the active operation queue is empty.
46 | ///
47 | ///
48 | /// Performed execution will include all due scheduled operations, but will not wait for operations scheduled for after the active operation queue has been emptied nor the callbacks of awaited operations that did not finish yet.
49 | ///
50 | ///
51 | public void Execute()
52 | {
53 | var oldDispatcher = _current;
54 | var oldSynchronizationContext = SynchronizationContext.Current;
55 |
56 | try
57 | {
58 | _current = this;
59 | SynchronizationContext.SetSynchronizationContext(_synchronizationContext);
60 |
61 | _synchronizationContext.Execute();
62 | }
63 | finally
64 | {
65 | _current = oldDispatcher;
66 | SynchronizationContext.SetSynchronizationContext(oldSynchronizationContext);
67 | }
68 | }
69 |
70 | ///
71 | /// Stops the current execution that was started be either calling the or the method.
72 | ///
73 | ///
74 | /// If called from within an operation currently executed by the dispatcher, the operation itself will not be terminated, but no new operations will be acquired from the operation queue once the current operation finishes or yields.
75 | ///
76 | public void Stop()
77 | {
78 | _synchronizationContext.Stop();
79 | }
80 |
81 | ///
82 | /// Verifies if this is the
83 | ///
84 | /// true if this instance of the is currently running on the calling thread.
85 | public bool CheckAccess()
86 | {
87 | return this == Current;
88 | }
89 |
90 | ///
91 | /// Creates a new and starts it on a new thread.
92 | ///
93 | ///
94 | public static Dispatcher Spawn()
95 | {
96 | var dispatcher = new Dispatcher();
97 | var thread = new Thread(() => {
98 | dispatcher.Start();
99 | });
100 | thread.Start();
101 |
102 | return dispatcher;
103 | }
104 |
105 | ///
106 | /// Creates a new and starts it on a new thread.
107 | ///
108 | /// Initial operation
109 | ///
110 | public static Dispatcher Spawn(Action operation)
111 | {
112 | var dispatcher = Spawn();
113 | dispatcher.Dispatch(DispatchPriority.High, operation);
114 | return dispatcher;
115 | }
116 |
117 | ///
118 | /// Adds the to the operation queue and stops the current thread until it's executed. The operation will be queued with a default priority.
119 | ///
120 | /// Operation to be queued
121 | /// If called from within an operation currently executed by the , the provided will be performed in place to avoid deadlocks.
122 | public void Invoke(Action operation) => Invoke(DispatchPriority.Medium, operation);
123 | ///
124 | /// Adds the to the operation queue and stops the current thread until it's executed.
125 | ///
126 | /// Priority of the operation
127 | /// Operation to be queued
128 | /// If called from within an operation currently executed by the , the provided will be performed in place to avoid deadlocks.
129 | public void Invoke(DispatchPriority priority, Action operation)
130 | {
131 | if (CheckAccess())
132 | operation();
133 | else
134 | _synchronizationContext.Send(priority, operation);
135 | }
136 |
137 | ///
138 | /// Adds the to the operation queue and stops the current thread until it's executed. The operation will be queued with a default priority.
139 | ///
140 | /// Operation to be queued
141 | /// Result value of the performed operation
142 | /// If called from within an operation currently executed by the , the provided will be performed in place to avoid deadlocks.
143 | public T Invoke(Func operation) => Invoke(DispatchPriority.Medium, operation);
144 | ///
145 | /// Adds the to the operation queue and stops the current thread until it's executed.
146 | ///
147 | /// Priority of the operation
148 | /// Operation to be queued
149 | /// Result value of the performed operation
150 | /// If called from within an operation currently executed by the , the provided will be performed in place to avoid deadlocks.
151 | public T Invoke(DispatchPriority priority, Func operation)
152 | {
153 | if (CheckAccess())
154 | return operation();
155 | else
156 | return InvokeAsync(priority, operation).Result;
157 | }
158 |
159 | ///
160 | /// Adds the to the operation queue and returns a associated with the state of its execution. The operation will be queued with a default priority.
161 | ///
162 | /// Operation to be queued
163 | public Task InvokeAsync(Action operation) => InvokeAsync(DispatchPriority.Medium, operation);
164 | ///
165 | /// Adds the to the operation queue and returns a associated with the state of its execution.
166 | ///
167 | /// Priority of the operation
168 | /// Operation to be queued
169 | public Task InvokeAsync(DispatchPriority priority, Action operation)
170 | {
171 | return _synchronizationContext.Send(priority, operation);
172 | }
173 |
174 | ///
175 | /// Adds the asynchronous to the operation queue and returns a associated with the state of its execution. The operation will be queued with a default priority.
176 | ///
177 | /// Operation to be queued
178 | public Task InvokeAsync(Func operation) => InvokeAsync(DispatchPriority.Medium, operation);
179 | ///
180 | /// Adds the asynchronous to the operation queue and returns a associated with the state of its execution.
181 | ///
182 | /// Priority of the operation
183 | /// Operation to be queued
184 | public async Task InvokeAsync(DispatchPriority priority, Func operation)
185 | {
186 | await await InvokeAsync(priority, operation);
187 | }
188 |
189 | ///
190 | /// Adds the to the operation queue and returns a associated with the state of its execution. The operation will be queued with a default priority.
191 | ///
192 | /// Operation to be queued
193 | public Task InvokeAsync(Func operation) => InvokeAsync(DispatchPriority.Medium, operation);
194 | ///
195 | /// Adds the to the operation queue and returns a associated with the state of its execution.
196 | ///
197 | /// Priority of the operation
198 | /// Operation to be queued
199 | public Task InvokeAsync(DispatchPriority priority, Func operation)
200 | {
201 | return _synchronizationContext.Send(priority, operation);
202 | }
203 |
204 | ///
205 | /// Adds the asynchronous to the operation queue and returns a associated with the state of its execution. The operation will be queued with a default priority.
206 | ///
207 | /// Operation to be queued
208 | public Task InvokeAsync(Func> operation) => InvokeAsync(DispatchPriority.Medium, operation);
209 | ///
210 | /// Adds the asynchronous to the operation queue and returns a associated with the state of its execution.
211 | ///
212 | /// Priority of the operation
213 | /// Operation to be queued
214 | public async Task InvokeAsync(DispatchPriority priority, Func> operation)
215 | {
216 | return await await InvokeAsync>(priority, operation);
217 | }
218 |
219 | ///
220 | /// Adds the to the operation queue without blocking of the current thread (fire and forget). The operation will be queued with a default priority.
221 | ///
222 | /// Operation to be queued
223 | public void Dispatch(Action operation) => Dispatch(DispatchPriority.Medium, operation);
224 | ///
225 | /// Adds the to the operation queue without blocking of the current thread (fire and forget).
226 | ///
227 | /// Priority of the operation
228 | /// Operation to be queued
229 | public void Dispatch(DispatchPriority priority, Action operation)
230 | {
231 | _synchronizationContext.Post(priority, operation);
232 | }
233 |
234 | ///
235 | /// Adds the asynchronous to the operation queue without blocking of the current thread (fire and forget). The operation will be queued with a default priority.
236 | ///
237 | /// Operation to be queued
238 | public void Dispatch(Func operation) => Dispatch(DispatchPriority.Medium, operation);
239 | ///
240 | /// Adds the asynchronous to the operation queue without blocking of the current thread (fire and forget).
241 | ///
242 | /// Priority of the operation
243 | /// Operation to be queued
244 | public void Dispatch(DispatchPriority priority, Func operation)
245 | {
246 | _synchronizationContext.Post(priority, () => operation());
247 | }
248 |
249 | ///
250 | /// If called for another thread, works similarly to the , but if called from the thread - executes the inline.
251 | ///
252 | /// Operation to be queued
253 | public void Run(Action operation) => Run(DispatchPriority.Medium, operation);
254 | ///
255 | /// If called for another thread, works similarly to the , but if called from the thread - executes the inline.
256 | ///
257 | /// Priority of the operation
258 | /// Operation to be queued
259 | public void Run(DispatchPriority priority, Action operation)
260 | {
261 | if (CheckAccess())
262 | operation();
263 | else
264 | Dispatch(priority, operation);
265 | }
266 |
267 | ///
268 | /// If called for another thread, works similarly to the , but if called from the thread - executes the inline.
269 | ///
270 | /// Operation to be queued
271 | public void Run(Func operation) => Run(DispatchPriority.Medium, operation);
272 | ///
273 | /// If called for another thread, works similarly to the , but if called from the thread - executes the inline.
274 | ///
275 | /// Priority of the operation
276 | /// Operation to be queued
277 | public void Run(DispatchPriority priority, Func operation)
278 | {
279 | if (CheckAccess())
280 | operation();
281 | else
282 | Dispatch(priority, operation);
283 | }
284 |
285 | ///
286 | /// Schedules the execution of the after the provided . The operation will be queued with a default priority.
287 | ///
288 | ///
289 | /// Operation to be scheduled
290 | ///
291 | /// It is not guaranteed that the will be executed exactly after the provided (it's only guaranteed that it will be queued not sooner then that).
292 | ///
293 | public void Schedule(TimeSpan delay, Action operation) => Schedule(delay, DispatchPriority.Medium, operation);
294 | ///
295 | /// Schedules the execution of the after the provided .
296 | ///
297 | ///
298 | /// Operation to be scheduled
299 | ///
300 | /// It is not guaranteed that the will be executed exactly after the provided (it's only guaranteed that it will be queued not sooner then that).
301 | ///
302 | public void Schedule(TimeSpan delay, DispatchPriority priority, Action operation)
303 | {
304 | _synchronizationContext.PostDelayed(DateTime.UtcNow + delay, priority, operation);
305 | }
306 |
307 | ///
308 | /// Schedules the execution of the asynchronous after the provided . The operation will be queued with a default priority.
309 | ///
310 | ///
311 | /// Operation to be scheduled
312 | ///
313 | /// It is not guaranteed that the will be executed exactly after the provided (it's only guaranteed that it will be queued not sooner then that).
314 | ///
315 | public void Schedule(TimeSpan delay, Func operation) => Schedule(delay, DispatchPriority.Medium, operation);
316 | ///
317 | /// Schedules the execution of the after the provided .
318 | ///
319 | ///
320 | /// Operation to be scheduled
321 | ///
322 | /// It is not guaranteed that the will be executed exactly after the provided (it's only guaranteed that it will be queued not sooner then that).
323 | ///
324 | public void Schedule(TimeSpan delay, DispatchPriority priority, Func operation)
325 | {
326 | _synchronizationContext.PostDelayed(DateTime.UtcNow + delay, priority, () => operation());
327 | }
328 |
329 | ///
330 | /// When awaited, will yield the execution of the current dispatcher to other queued operations with at least given .
331 | ///
332 | /// The priority of operations to be executed
333 | /// Throws the if no dispatcher is currently running on the calling thread.
334 | public static YieldTask Yield(DispatchPriority priority = DispatchPriority.Low)
335 | {
336 | return new YieldTask(priority);
337 | }
338 | }
339 | }
340 |
--------------------------------------------------------------------------------