├── 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 | --------------------------------------------------------------------------------