├── .editorconfig ├── .gitignore ├── Benchmarks ├── Program.cs ├── Benchmarks.csproj └── ParallelForeachBenchmark.cs ├── AwaitThreading.Core ├── AssemblyInfo.cs ├── Tasks │ ├── Unit.cs │ ├── IParallelNotifyCompletion.cs │ ├── ParallelTaskExtensions.cs │ ├── ParallelValueTask.cs │ ├── ParallelTaskResult.cs │ ├── ParallelValueTask`1.cs │ ├── ParallelTask.cs │ ├── IContinuationInvoker.cs │ ├── StateMachineExtensions.cs │ ├── ParallelTask`1.cs │ ├── ParallelTaskAwaiter.cs │ ├── ParallelTaskAwaiter`1.cs │ ├── ParallelTaskMethodBuilder.cs │ ├── ParallelTaskMethodBuilder`1.cs │ ├── ParallelValueTaskMethodBuilder.cs │ ├── ParallelValueTaskMethodBuilder`1.cs │ ├── ParallelValueTaskAwaiter.cs │ ├── ParallelValueTaskAwaiter`1.cs │ ├── ParallelTaskMethodBuilderImpl.cs │ └── ParallelTaskImpl.cs ├── Operations │ ├── ForkingOptions.cs │ ├── JoiningTask.cs │ ├── TargetedJoiningTask.cs │ └── ForkingTask.cs ├── AwaitThreading.Core.csproj ├── Diagnostics │ ├── Logger.cs │ └── Assertion.cs ├── Context │ ├── ParallelFrame.cs │ ├── SingleWaiterBarrier.cs │ ├── ParallelContextStorage.cs │ └── ParallelContext.cs ├── ParallelLocal.cs └── ParallelOperations.cs ├── AwaitThreading.Core.Tests ├── GlobalUsings.cs ├── Helpers │ ├── ConstraintExtensions.cs │ ├── ParallelCounter.cs │ ├── AssertEx.cs │ └── BaseClassWithParallelContextValidation.cs ├── AwaitThreading.Core.Tests.csproj ├── ForkTaskWithIdTests.cs ├── ConcurrencyTests.cs ├── Tasks │ ├── ParallelTaskExtensionsTests.cs │ └── ParallelValueTaskTest.cs ├── CoreOperationsTests.cs ├── ParallelTaskMethodBuilderTests.cs └── TaskOverParallelTaskTests.cs ├── AwaitThreading.Enumerable ├── IParallelAsyncEnumerable.cs ├── IParallelAsyncLazyForkingEnumerable.cs ├── IParallelAsyncEnumerator.cs ├── IParallelAsyncLazyForkingEnumerator.cs ├── AwaitThreading.Enumerable.csproj ├── Experimental │ └── ListExperimentalExtensions.cs ├── ParallelAsyncDelegatingEnumerable.cs ├── ParallelAsyncDelegatingEnumerator.cs ├── ParallelAsyncEnumerable.cs ├── SyncTask.cs ├── ParallelAsyncLazyForkingRangeEnumerable.cs ├── ParallelAsyncEnumerator.cs ├── ParallelAsyncLazyForkingPartitionEnumerable.cs ├── CollectionParallelExtensions.AsyncParallel.cs ├── ParallelAsyncLazyForkingPartitionEnumerator.cs ├── CollectionParallelExtensions.ParallelAsync.cs ├── ParallelAsyncLazyForkingRangeEnumerator.cs └── ParallelRangeManager.cs ├── Samples ├── Samples.csproj └── Program.cs ├── AwaitThreading.Enumerable.Tests ├── AwaitThreading.Enumerable.Tests.csproj ├── PartitionParallelExtensionsTest.cs └── CollectionParallelExtensionsTest.cs ├── AwaitThreading.sln.DotSettings ├── LICENSE ├── AwaitThreading.sln └── README.md /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | [*.cs] 3 | indent_style = space 4 | indent_size = 4 5 | tab_width = 4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | /packages/ 4 | riderModule.iml 5 | /_ReSharper.Caches/ 6 | .idea/ 7 | *.DotSettings.user -------------------------------------------------------------------------------- /Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Running; 2 | 3 | BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).RunAll(); -------------------------------------------------------------------------------- /AwaitThreading.Core/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("AwaitThreading.Core.Tests")] -------------------------------------------------------------------------------- /AwaitThreading.Core.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | global using AwaitThreading.Core.Tests.Helpers; 6 | global using NUnit.Framework; -------------------------------------------------------------------------------- /AwaitThreading.Core/Tasks/Unit.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2023 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Runtime.InteropServices; 6 | 7 | namespace AwaitThreading.Core.Tasks; 8 | 9 | [StructLayout(LayoutKind.Sequential, Size = 1)] 10 | internal struct Unit 11 | { 12 | } -------------------------------------------------------------------------------- /AwaitThreading.Enumerable/IParallelAsyncEnumerable.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace AwaitThreading.Enumerable; 6 | 7 | public interface IParallelAsyncEnumerable 8 | { 9 | IParallelAsyncEnumerator GetAsyncEnumerator(); 10 | } -------------------------------------------------------------------------------- /AwaitThreading.Enumerable/IParallelAsyncLazyForkingEnumerable.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace AwaitThreading.Enumerable; 6 | 7 | public interface IParallelAsyncLazyForkingEnumerable 8 | { 9 | IParallelAsyncLazyForkingEnumerator GetAsyncEnumerator(); 10 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Operations/ForkingOptions.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2025 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace AwaitThreading.Core.Operations; 6 | 7 | public sealed class ForkingOptions 8 | { 9 | public TaskCreationOptions? TaskCreationOptions { get; set; } 10 | public TaskScheduler? TaskScheduler { get; set; } 11 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/AwaitThreading.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Samples/Samples.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /AwaitThreading.Enumerable/IParallelAsyncEnumerator.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using AwaitThreading.Core.Operations; 6 | using JetBrains.Annotations; 7 | 8 | namespace AwaitThreading.Enumerable; 9 | 10 | public interface IParallelAsyncEnumerator 11 | { 12 | SyncTask MoveNextAsync(); 13 | 14 | T Current { get; } 15 | 16 | // TODO: ParallelTask would be more universal 17 | [UsedImplicitly] 18 | JoiningTask DisposeAsync(); 19 | } -------------------------------------------------------------------------------- /AwaitThreading.Enumerable/IParallelAsyncLazyForkingEnumerator.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using AwaitThreading.Core.Tasks; 6 | using JetBrains.Annotations; 7 | 8 | namespace AwaitThreading.Enumerable; 9 | 10 | public interface IParallelAsyncLazyForkingEnumerator 11 | { 12 | ParallelValueTask MoveNextAsync(); 13 | T Current { get; } 14 | 15 | //TODO: detect in usage analysis 16 | [UsedImplicitly] 17 | ParallelTask DisposeAsync(); 18 | } -------------------------------------------------------------------------------- /AwaitThreading.Core.Tests/Helpers/ConstraintExtensions.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using NUnit.Framework.Constraints; 6 | 7 | namespace AwaitThreading.Core.Tests.Helpers; 8 | 9 | public static class ConstraintExtensions 10 | { 11 | public static EqualConstraint UsingStringLinesCountEquality(this EqualConstraint equalConstraint) 12 | { 13 | return equalConstraint 14 | .Using((s1, s2) => s1.Split(Environment.NewLine).Length == s2.Split(Environment.NewLine).Length); 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /AwaitThreading.Enumerable/AwaitThreading.Enumerable.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /AwaitThreading.Enumerable/Experimental/ListExperimentalExtensions.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using AwaitThreading.Core.Operations; 6 | 7 | namespace AwaitThreading.Enumerable.Experimental; 8 | 9 | public static class ListExperimentalExtensions 10 | { 11 | //[Experimental] 12 | public static ParallelAsyncLazyForkingRangeEnumerator GetAsyncEnumerator(this IReadOnlyList list, ForkingOptions? forkingOptions = null) 13 | { 14 | return new ParallelAsyncLazyForkingRangeEnumerator(list, Environment.ProcessorCount, forkingOptions); 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /AwaitThreading.Core.Tests/AwaitThreading.Core.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Benchmarks/Benchmarks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /AwaitThreading.Core/Tasks/IParallelNotifyCompletion.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2023 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace AwaitThreading.Core.Tasks; 8 | 9 | public interface IParallelNotifyCompletion 10 | { 11 | /// 12 | /// Same as AwaitUnsafeOnCompleted, but with ParallelTask scenarios support: 13 | /// Implementations are required to restore the execution context when invoking stateMachine.MoveNext 14 | /// 15 | void ParallelOnCompleted(TStateMachine stateMachine) 16 | where TStateMachine : IAsyncStateMachine; 17 | } -------------------------------------------------------------------------------- /AwaitThreading.Enumerable.Tests/AwaitThreading.Enumerable.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /AwaitThreading.Core.Tests/Helpers/ParallelCounter.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace AwaitThreading.Core.Tests.Helpers; 6 | 7 | public sealed class ParallelCounter 8 | { 9 | private int _count; 10 | 11 | public int Count => _count; 12 | 13 | public void Increment() 14 | { 15 | Interlocked.Increment(ref _count); 16 | } 17 | 18 | public void Add(int value) 19 | { 20 | Interlocked.Add(ref _count, value); 21 | } 22 | 23 | public void AssertCount(int expectedCount) 24 | { 25 | Assert.That(_count, Is.EqualTo(expectedCount)); 26 | } 27 | } -------------------------------------------------------------------------------- /AwaitThreading.Core.Tests/Helpers/AssertEx.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace AwaitThreading.Core.Tests.Helpers; 6 | 7 | public static class AssertEx 8 | { 9 | public static async Task CheckThrowsAsync(Func testFunc) where TException : Exception 10 | { 11 | try 12 | { 13 | await testFunc.Invoke(); 14 | } 15 | catch (Exception exception) 16 | { 17 | Assert.That(exception, Is.InstanceOf()); 18 | return; 19 | } 20 | 21 | Assert.Fail("No exception is thrown"); 22 | } 23 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Diagnostics/Logger.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Diagnostics; 6 | using AwaitThreading.Core.Context; 7 | 8 | namespace AwaitThreading.Core.Diagnostics; 9 | 10 | public static class Logger 11 | { 12 | private static Stopwatch? _stopwatch; 13 | 14 | [Conditional("DEBUG")] 15 | public static void Log(string message) 16 | { 17 | _stopwatch ??= Stopwatch.StartNew(); 18 | Console.Out.WriteLine($"{_stopwatch.ElapsedTicks:0000000000}/{_stopwatch.ElapsedMilliseconds:00000} [id={Thread.CurrentThread.ManagedThreadId}, context={ParallelContextStorage.CurrentThreadContext.StackToString()}]: {message}"); 19 | } 20 | } -------------------------------------------------------------------------------- /AwaitThreading.Enumerable/ParallelAsyncDelegatingEnumerable.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace AwaitThreading.Enumerable; 6 | 7 | public class ParallelAsyncDelegatingEnumerable : IParallelAsyncEnumerable 8 | { 9 | private readonly IEnumerator _enumerator; 10 | 11 | public ParallelAsyncDelegatingEnumerable(IEnumerator enumerator) 12 | { 13 | _enumerator = enumerator; 14 | } 15 | 16 | IParallelAsyncEnumerator IParallelAsyncEnumerable.GetAsyncEnumerator() 17 | { 18 | return GetAsyncEnumerator(); 19 | } 20 | 21 | public ParallelAsyncDelegatingEnumerator GetAsyncEnumerator() 22 | { 23 | return new ParallelAsyncDelegatingEnumerator(_enumerator); 24 | } 25 | } -------------------------------------------------------------------------------- /AwaitThreading.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | Copyright (c) ${File.CreatedYear} Saltuk Konstantin 4 | See the LICENSE file in the project root for more information. 5 | 6 | True 7 | True -------------------------------------------------------------------------------- /AwaitThreading.Core/Tasks/ParallelTaskExtensions.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2023 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace AwaitThreading.Core.Tasks; 6 | 7 | public static class ParallelTaskExtensions 8 | { 9 | public static async Task AsTask(this ParallelTask parallelTask) 10 | { 11 | await parallelTask; 12 | } 13 | 14 | public static async Task AsTask(this ParallelTask parallelTask) 15 | { 16 | return await parallelTask; 17 | } 18 | 19 | public static ValueTask AsValueTask(this ParallelValueTask parallelValueTask) 20 | { 21 | return parallelValueTask.Implementation is { } implementation 22 | ? new ValueTask(new ParallelTask(implementation).AsTask()) 23 | : ValueTask.FromResult(parallelValueTask.Result!); 24 | } 25 | } -------------------------------------------------------------------------------- /AwaitThreading.Enumerable/ParallelAsyncDelegatingEnumerator.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using AwaitThreading.Core.Operations; 6 | 7 | namespace AwaitThreading.Enumerable; 8 | 9 | public class ParallelAsyncDelegatingEnumerator : IParallelAsyncEnumerator 10 | { 11 | private readonly IEnumerator _enumerator; 12 | 13 | internal ParallelAsyncDelegatingEnumerator(IEnumerator enumerator) 14 | { 15 | _enumerator = enumerator; 16 | } 17 | 18 | public SyncTask MoveNextAsync() 19 | { 20 | return new SyncTask(_enumerator.MoveNext()); 21 | } 22 | 23 | public T Current => _enumerator.Current; 24 | 25 | public JoiningTask DisposeAsync() 26 | { 27 | _enumerator.Dispose(); 28 | return new JoiningTask(); 29 | } 30 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Tasks/ParallelValueTask.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace AwaitThreading.Core.Tasks; 8 | 9 | [AsyncMethodBuilder(typeof(ParallelValueTaskMethodBuilder))] 10 | public readonly struct ParallelValueTask 11 | { 12 | public static ParallelValueTask CompletedTask => new(); 13 | public static ParallelValueTask FromResult(T result) => new(result); 14 | 15 | internal readonly ParallelTaskImpl? Implementation; 16 | 17 | public ParallelValueTask() 18 | { 19 | Implementation = null; 20 | } 21 | 22 | internal ParallelValueTask(ParallelTaskImpl parallelTaskImpl) 23 | { 24 | Implementation = parallelTaskImpl; 25 | } 26 | 27 | public ParallelValueTaskAwaiter GetAwaiter() => new(this); 28 | 29 | } -------------------------------------------------------------------------------- /AwaitThreading.Enumerable/ParallelAsyncEnumerable.cs: -------------------------------------------------------------------------------- 1 | //MIT License 2 | //Copyright (c) 2023 Saltuk Konstantin 3 | //See the LICENSE file in the project root for more information. 4 | 5 | namespace AwaitThreading.Enumerable; 6 | 7 | public readonly struct ParallelAsyncEnumerable : IParallelAsyncEnumerable 8 | { 9 | private readonly IReadOnlyList _list; 10 | private readonly RangeWorker _rangeWorker; 11 | 12 | internal ParallelAsyncEnumerable(IReadOnlyList list, RangeWorker rangeWorker) 13 | { 14 | _list = list; 15 | _rangeWorker = rangeWorker; 16 | } 17 | 18 | IParallelAsyncEnumerator IParallelAsyncEnumerable.GetAsyncEnumerator() 19 | { 20 | return GetAsyncEnumerator(); 21 | } 22 | 23 | public ParallelAsyncEnumerator GetAsyncEnumerator() 24 | { 25 | return new ParallelAsyncEnumerator(_list, _rangeWorker); 26 | } 27 | } -------------------------------------------------------------------------------- /AwaitThreading.Core.Tests/ForkTaskWithIdTests.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2025 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Collections.Concurrent; 6 | using AwaitThreading.Core.Tasks; 7 | 8 | namespace AwaitThreading.Core.Tests; 9 | 10 | [TestOf(typeof(ForkingTaskWithId))] 11 | public class ForkTaskWithIdTests : BaseClassWithParallelContextValidation 12 | { 13 | [TestCase(1)] 14 | [TestCase(2)] 15 | [TestCase(10)] 16 | public async Task Fork_NThreadsStarted_NThreadsExecuted(int n) 17 | { 18 | var ids = new ConcurrentBag(); 19 | await TestBody(); 20 | Assert.That(ids, Is.EquivalentTo(Enumerable.Range(0, n))); 21 | return; 22 | 23 | async ParallelTask TestBody() 24 | { 25 | var id = await ParallelOperations.Fork(n); 26 | ids.Add(id); 27 | await ParallelOperations.Join(); 28 | } 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Tasks/ParallelTaskResult.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Diagnostics.CodeAnalysis; 6 | using System.Runtime.ExceptionServices; 7 | 8 | namespace AwaitThreading.Core.Tasks; 9 | 10 | internal readonly struct ParallelTaskResult 11 | { 12 | public ParallelTaskResult(T result) 13 | { 14 | Result = result; 15 | ExceptionDispatchInfo = null; 16 | } 17 | 18 | public ParallelTaskResult(ExceptionDispatchInfo exceptionDispatchInfo) 19 | { 20 | ExceptionDispatchInfo = exceptionDispatchInfo; 21 | Result = default; 22 | } 23 | 24 | [MemberNotNullWhen(true, nameof(Result))] 25 | [MemberNotNullWhen(false, nameof(ExceptionDispatchInfo))] 26 | public bool HasResult => ExceptionDispatchInfo is null; 27 | 28 | public readonly T? Result; 29 | 30 | public readonly ExceptionDispatchInfo? ExceptionDispatchInfo; 31 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Tasks/ParallelValueTask`1.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace AwaitThreading.Core.Tasks; 8 | 9 | [AsyncMethodBuilder(typeof(ParallelValueTaskMethodBuilder<>))] 10 | public readonly struct ParallelValueTask 11 | { 12 | // Note: we could implement a sync exception workload, but ParallelValueTask is implemented only 13 | // for performance optimizations and exceptions are not a priority here. 14 | internal readonly T? Result; 15 | internal readonly ParallelTaskImpl? Implementation; 16 | 17 | public ParallelValueTask(T result) 18 | { 19 | Result = result; 20 | Implementation = null; 21 | } 22 | 23 | internal ParallelValueTask(ParallelTaskImpl parallelTaskImpl) 24 | { 25 | Result = default; 26 | Implementation = parallelTaskImpl; 27 | } 28 | 29 | public ParallelValueTaskAwaiter GetAwaiter() => new(this); 30 | } -------------------------------------------------------------------------------- /AwaitThreading.Enumerable/SyncTask.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace AwaitThreading.Enumerable; 8 | 9 | /// 10 | /// Even more optimized version of ValueTask that supports only sync returns. Should be free in terms of overhead after inlining 11 | /// 12 | public readonly struct SyncTask : INotifyCompletion 13 | { 14 | private readonly T _result; 15 | 16 | public SyncTask(T result) 17 | { 18 | _result = result; 19 | } 20 | 21 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 22 | public SyncTask GetAwaiter() => this; 23 | 24 | public bool IsCompleted 25 | { 26 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 27 | get => true; 28 | } 29 | 30 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 31 | public T GetResult() => _result; 32 | 33 | public void OnCompleted(Action continuation) 34 | { 35 | } 36 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Tasks/ParallelTask.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2023 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Runtime.CompilerServices; 6 | using System.Runtime.ExceptionServices; 7 | 8 | namespace AwaitThreading.Core.Tasks; 9 | 10 | [AsyncMethodBuilder(typeof(ParallelTaskMethodBuilder))] 11 | public readonly struct ParallelTask 12 | { 13 | internal readonly ParallelTaskImpl Implementation; 14 | 15 | public ParallelTask() 16 | { 17 | Implementation = new ParallelTaskImpl(); 18 | } 19 | 20 | internal void SetResult() => Implementation.SetResult(default); 21 | 22 | internal void SetException(Exception e) => 23 | Implementation.SetResult(new ParallelTaskResult(ExceptionDispatchInfo.Capture(e))); 24 | 25 | internal void SetStateMachine(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine 26 | => Implementation.SetStateMachine(ref stateMachine); 27 | 28 | public ParallelTaskAwaiter GetAwaiter() => new(Implementation); 29 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Tasks/IContinuationInvoker.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace AwaitThreading.Core.Tasks; 8 | 9 | internal interface IContinuationInvoker 10 | { 11 | void Invoke(); 12 | } 13 | 14 | internal sealed class ParallelContinuationInvoker : IContinuationInvoker 15 | where TStateMachine : IAsyncStateMachine 16 | { 17 | private readonly TStateMachine _stateMachine; 18 | 19 | public ParallelContinuationInvoker(TStateMachine stateMachine) 20 | { 21 | _stateMachine = stateMachine.MakeCopy(); 22 | } 23 | 24 | public void Invoke() 25 | { 26 | _stateMachine.MakeCopy().MoveNext(); 27 | } 28 | } 29 | 30 | // internal sealed class TaskFinishedMarker : IContinuationInvoker 31 | // { 32 | // public static readonly TaskFinishedMarker Instance = new(); 33 | // 34 | // private TaskFinishedMarker() 35 | // { 36 | // } 37 | // 38 | // public void Invoke() 39 | // { 40 | // } 41 | // } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Saltuk Konstantin 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 | -------------------------------------------------------------------------------- /AwaitThreading.Core/Context/ParallelFrame.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2025 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace AwaitThreading.Core.Context; 6 | 7 | public readonly struct ParallelFrame : IEquatable 8 | { 9 | public readonly int Id; 10 | internal readonly SingleWaiterBarrier JoinBarrier; 11 | 12 | #if DEBUG 13 | public readonly string CreationStackTrace; 14 | #endif 15 | 16 | internal ParallelFrame(int id, SingleWaiterBarrier joinBarrier) 17 | { 18 | Id = id; 19 | JoinBarrier = joinBarrier; 20 | #if DEBUG 21 | CreationStackTrace = Environment.StackTrace; 22 | #endif 23 | } 24 | 25 | public object ForkIdentity => JoinBarrier; 26 | 27 | public bool Equals(ParallelFrame other) 28 | { 29 | return Id == other.Id && JoinBarrier.Equals(other.JoinBarrier); 30 | } 31 | 32 | public override bool Equals(object? obj) 33 | { 34 | return obj is ParallelFrame other && Equals(other); 35 | } 36 | 37 | public override int GetHashCode() 38 | { 39 | return HashCode.Combine(Id, JoinBarrier); 40 | } 41 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Tasks/StateMachineExtensions.cs: -------------------------------------------------------------------------------- 1 | //MIT License 2 | //Copyright (c) 2023 Saltuk Konstantin 3 | //See the LICENSE file in the project root for more information. 4 | 5 | using System.Reflection; 6 | using System.Runtime.CompilerServices; 7 | 8 | namespace AwaitThreading.Core.Tasks; 9 | 10 | internal static class StateMachineExtensions 11 | { 12 | private static readonly MethodInfo CloneMethod = 13 | typeof(object).GetMethod("MemberwiseClone", BindingFlags.NonPublic | BindingFlags.Instance)!; 14 | 15 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 16 | public static TStateMachine MakeCopy(this TStateMachine stateMachine) 17 | where TStateMachine : IAsyncStateMachine 18 | { 19 | if (typeof(TStateMachine).IsValueType) 20 | { 21 | // in release mode this method should be inlined with no copy overhead at all 22 | return stateMachine; 23 | } 24 | 25 | return (TStateMachine)Copy(stateMachine); 26 | } 27 | 28 | private static object Copy(object originalObject) 29 | { 30 | return CloneMethod.Invoke(originalObject, null)!; //TODO to compiled 31 | } 32 | } -------------------------------------------------------------------------------- /AwaitThreading.Enumerable/ParallelAsyncLazyForkingRangeEnumerable.cs: -------------------------------------------------------------------------------- 1 | //MIT License 2 | //Copyright (c) 2023 Saltuk Konstantin 3 | //See the LICENSE file in the project root for more information. 4 | 5 | using AwaitThreading.Core.Operations; 6 | 7 | namespace AwaitThreading.Enumerable; 8 | 9 | public sealed class ParallelAsyncLazyForkingRangeEnumerable : IParallelAsyncLazyForkingEnumerable 10 | { 11 | private readonly IReadOnlyList _list; 12 | private readonly int _threadCount; 13 | private readonly ForkingOptions? _forkingOptions; 14 | 15 | public ParallelAsyncLazyForkingRangeEnumerable(IReadOnlyList list, int threadCount, ForkingOptions? forkingOptions) 16 | { 17 | _list = list; 18 | _threadCount = threadCount; 19 | _forkingOptions = forkingOptions; 20 | } 21 | 22 | IParallelAsyncLazyForkingEnumerator IParallelAsyncLazyForkingEnumerable.GetAsyncEnumerator() 23 | { 24 | return GetAsyncEnumerator(); 25 | } 26 | 27 | public ParallelAsyncLazyForkingRangeEnumerator GetAsyncEnumerator() 28 | { 29 | return new ParallelAsyncLazyForkingRangeEnumerator(_list, _threadCount, _forkingOptions); 30 | } 31 | } -------------------------------------------------------------------------------- /AwaitThreading.Enumerable/ParallelAsyncEnumerator.cs: -------------------------------------------------------------------------------- 1 | //MIT License 2 | //Copyright (c) 2023 Saltuk Konstantin 3 | //See the LICENSE file in the project root for more information. 4 | 5 | using AwaitThreading.Core.Operations; 6 | 7 | namespace AwaitThreading.Enumerable; 8 | 9 | public struct ParallelAsyncEnumerator : IParallelAsyncEnumerator 10 | { 11 | private readonly IReadOnlyList _list; 12 | private RangeWorker _rangeWorker; 13 | private int _fromInclusive; 14 | private int _toExclusive; 15 | 16 | internal ParallelAsyncEnumerator(IReadOnlyList list, RangeWorker rangeWorker) 17 | { 18 | _list = list; 19 | _rangeWorker = rangeWorker; 20 | _fromInclusive = 0; 21 | _toExclusive = 0; 22 | } 23 | 24 | public SyncTask MoveNextAsync() 25 | { 26 | if (_fromInclusive++ >= _toExclusive - 1) 27 | { 28 | return new SyncTask(_rangeWorker.FindNewWork(out _fromInclusive, out _toExclusive)); 29 | } 30 | 31 | return new SyncTask(true); 32 | } 33 | 34 | public T Current => _list[_fromInclusive]; 35 | 36 | public JoiningTask DisposeAsync() 37 | { 38 | return new JoiningTask(); 39 | } 40 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Tasks/ParallelTask`1.cs: -------------------------------------------------------------------------------- 1 | //MIT License 2 | //Copyright (c) 2023 Saltuk Konstantin 3 | //See the LICENSE file in the project root for more information. 4 | 5 | using System.Runtime.CompilerServices; 6 | using System.Runtime.ExceptionServices; 7 | 8 | namespace AwaitThreading.Core.Tasks; 9 | 10 | [AsyncMethodBuilder(typeof(ParallelTaskMethodBuilder<>))] 11 | public readonly struct ParallelTask 12 | { 13 | internal readonly ParallelTaskImpl Implementation; 14 | 15 | public ParallelTask() 16 | { 17 | Implementation = new ParallelTaskImpl(); 18 | } 19 | 20 | internal ParallelTask(ParallelTaskImpl implementation) 21 | { 22 | Implementation = implementation; 23 | } 24 | 25 | internal void SetResult(T result) => 26 | Implementation.SetResult(new ParallelTaskResult(result)); 27 | 28 | internal void SetException(Exception e) => 29 | Implementation.SetResult(new ParallelTaskResult(ExceptionDispatchInfo.Capture(e))); 30 | 31 | internal void SetStateMachine(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine => 32 | Implementation.SetStateMachine(ref stateMachine); 33 | 34 | public ParallelTaskAwaiter GetAwaiter() => new(Implementation); 35 | } -------------------------------------------------------------------------------- /AwaitThreading.Enumerable/ParallelAsyncLazyForkingPartitionEnumerable.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Collections.Concurrent; 6 | using AwaitThreading.Core.Operations; 7 | 8 | namespace AwaitThreading.Enumerable; 9 | 10 | public sealed class ParallelAsyncLazyForkingPartitionEnumerable : IParallelAsyncLazyForkingEnumerable 11 | { 12 | private readonly int _threadCount; 13 | private readonly ForkingOptions? _forkingOptions; 14 | private readonly Partitioner _partitioner; 15 | 16 | public ParallelAsyncLazyForkingPartitionEnumerable(Partitioner partitioner, int threadCount, ForkingOptions? forkingOptions) 17 | { 18 | _partitioner = partitioner; 19 | _threadCount = threadCount; 20 | _forkingOptions = forkingOptions; 21 | } 22 | 23 | IParallelAsyncLazyForkingEnumerator IParallelAsyncLazyForkingEnumerable.GetAsyncEnumerator() 24 | { 25 | return GetAsyncEnumerator(); 26 | } 27 | 28 | public ParallelAsyncLazyForkingPartitionEnumerator GetAsyncEnumerator() 29 | { 30 | return new ParallelAsyncLazyForkingPartitionEnumerator(_partitioner, _threadCount, _forkingOptions); 31 | } 32 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Tasks/ParallelTaskAwaiter.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2023 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace AwaitThreading.Core.Tasks; 8 | 9 | public readonly struct ParallelTaskAwaiter : ICriticalNotifyCompletion, IParallelNotifyCompletion 10 | { 11 | private readonly ParallelTaskImpl _taskImpl; 12 | 13 | internal ParallelTaskAwaiter(ParallelTaskImpl taskImpl) 14 | { 15 | _taskImpl = taskImpl; 16 | } 17 | 18 | public bool IsCompleted => false; 19 | 20 | public void ParallelOnCompleted(TStateMachine stateMachine) 21 | where TStateMachine : IAsyncStateMachine 22 | { 23 | _taskImpl.ParallelOnCompleted(stateMachine); 24 | } 25 | 26 | public void OnCompleted(Action continuation) => _taskImpl.OnCompleted(continuation); 27 | 28 | public void UnsafeOnCompleted(Action continuation) => _taskImpl.UnsafeOnCompleted(continuation); 29 | 30 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 31 | public void GetResult() 32 | { 33 | var taskResult = _taskImpl.GetResult(); 34 | if (!taskResult.HasResult) 35 | { 36 | taskResult.ExceptionDispatchInfo.Throw(); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Tasks/ParallelTaskAwaiter`1.cs: -------------------------------------------------------------------------------- 1 | //MIT License 2 | //Copyright (c) 2023 Saltuk Konstantin 3 | //See the LICENSE file in the project root for more information. 4 | 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace AwaitThreading.Core.Tasks; 8 | 9 | public readonly struct ParallelTaskAwaiter : ICriticalNotifyCompletion, IParallelNotifyCompletion 10 | { 11 | private readonly ParallelTaskImpl _taskImpl; 12 | 13 | internal ParallelTaskAwaiter(ParallelTaskImpl taskImpl) 14 | { 15 | _taskImpl = taskImpl; 16 | } 17 | 18 | public bool IsCompleted => false; 19 | 20 | public void ParallelOnCompleted(TStateMachine stateMachine) 21 | where TStateMachine : IAsyncStateMachine 22 | { 23 | _taskImpl.ParallelOnCompleted(stateMachine); 24 | } 25 | 26 | public void OnCompleted(Action continuation) => _taskImpl.OnCompleted(continuation); 27 | 28 | public void UnsafeOnCompleted(Action continuation) => _taskImpl.UnsafeOnCompleted(continuation); 29 | 30 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 31 | public T GetResult() 32 | { 33 | var taskResult = _taskImpl.GetResult(); 34 | if (!taskResult.HasResult) 35 | { 36 | taskResult.ExceptionDispatchInfo.Throw(); 37 | } 38 | 39 | return taskResult.Result; 40 | } 41 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/ParallelLocal.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using AwaitThreading.Core.Context; 6 | using AwaitThreading.Core.Diagnostics; 7 | using AwaitThreading.Core.Operations; 8 | using JetBrains.Annotations; 9 | 10 | namespace AwaitThreading.Core; 11 | 12 | /// 13 | /// Represents ambient data that is local to a given forked thread. 14 | /// 15 | /// 16 | /// Usage flow: ParallelLocal needs to be created before forking, 17 | /// then, forking should be performed using the method. 18 | /// After that, each thread can get and set its local value 19 | /// 20 | public class ParallelLocal 21 | { 22 | private T?[]? _slots; 23 | 24 | [MustUseReturnValue] 25 | public ForkingTask InitializeAndFork(int threadCount, ForkingOptions? forkingOptions = null) 26 | { 27 | if (_slots is not null) 28 | Assertion.ThrowInvalidParallelLocalUsage(); 29 | 30 | _slots = new T[threadCount]; 31 | return new ForkingTask(threadCount, forkingOptions); 32 | } 33 | 34 | public bool IsInitialized => _slots is not null; 35 | 36 | public ref T? Value 37 | { 38 | get 39 | { 40 | if (_slots is null) 41 | Assertion.ThrowInvalidParallelLocalUsage(); 42 | 43 | var id = ParallelContextStorage.GetTopFrameId(); 44 | return ref _slots[id]; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Operations/JoiningTask.cs: -------------------------------------------------------------------------------- 1 | //MIT License 2 | //Copyright (c) 2023 Saltuk Konstantin 3 | //See the LICENSE file in the project root for more information. 4 | 5 | using System.Runtime.CompilerServices; 6 | using AwaitThreading.Core.Context; 7 | using AwaitThreading.Core.Diagnostics; 8 | using AwaitThreading.Core.Tasks; 9 | 10 | namespace AwaitThreading.Core.Operations; 11 | 12 | public readonly struct JoiningTask 13 | { 14 | public readonly struct JoiningTaskAwaiter : ICriticalNotifyCompletion, IParallelNotifyCompletion 15 | { 16 | public bool IsCompleted => false; 17 | 18 | public void ParallelOnCompleted(TStateMachine stateMachine) 19 | where TStateMachine : IAsyncStateMachine 20 | { 21 | Logger.Log("Start joining"); 22 | var context = ParallelContextStorage.PopFrame(); 23 | 24 | if (context.JoinBarrier.Finish()) 25 | { 26 | stateMachine.MoveNext(); 27 | } 28 | else 29 | { 30 | ParallelContextStorage.CaptureAndClear(); 31 | } 32 | } 33 | 34 | public void OnCompleted(Action continuation) 35 | { 36 | Assertion.ThrowBadAwait(); 37 | } 38 | 39 | public void UnsafeOnCompleted(Action continuation) 40 | { 41 | Assertion.ThrowBadAwait(); 42 | } 43 | 44 | public void GetResult() 45 | { 46 | } 47 | } 48 | 49 | public JoiningTaskAwaiter GetAwaiter() => new(); 50 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Context/SingleWaiterBarrier.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2023 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using AwaitThreading.Core.Diagnostics; 6 | 7 | namespace AwaitThreading.Core.Context; 8 | 9 | internal sealed class SingleWaiterBarrier 10 | { 11 | internal int Count; 12 | 13 | public SingleWaiterBarrier(int count) 14 | { 15 | Count = count; 16 | } 17 | 18 | public bool Finish() 19 | { 20 | var decrementedValue = Interlocked.Decrement(ref Count); 21 | if (decrementedValue < 0) 22 | { 23 | Assertion.StateCorrupted("Too many threads signaled"); 24 | } 25 | 26 | return decrementedValue == 0; 27 | } 28 | 29 | public void Signal() 30 | { 31 | lock (this) 32 | { 33 | Count--; 34 | if (Count < 0) 35 | { 36 | Assertion.StateCorrupted("Too many threads signaled"); 37 | } 38 | 39 | if (Count == 0) 40 | { 41 | Monitor.Pulse(this); 42 | } 43 | } 44 | } 45 | 46 | public void SignalAndWait() 47 | { 48 | lock (this) 49 | { 50 | Count--; 51 | if (Count < 0) 52 | { 53 | Assertion.StateCorrupted("Too many threads signaled"); 54 | } 55 | 56 | while (Count != 0) 57 | { 58 | Monitor.Wait(this); 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /AwaitThreading.Enumerable/CollectionParallelExtensions.AsyncParallel.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Collections.Concurrent; 6 | using AwaitThreading.Core.Operations; 7 | 8 | namespace AwaitThreading.Enumerable; 9 | 10 | public static partial class CollectionParallelExtensions 11 | { 12 | public static ParallelAsyncLazyForkingRangeEnumerable AsAsyncParallel( 13 | this IReadOnlyList list, 14 | int threadCount, 15 | ForkingOptions? forkingOptions = null) 16 | { 17 | return new ParallelAsyncLazyForkingRangeEnumerable(list, threadCount, forkingOptions); 18 | } 19 | 20 | public static ParallelAsyncLazyForkingPartitionEnumerable AsAsyncParallel( 21 | this Partitioner partitioner, 22 | int threadCount, 23 | ForkingOptions? forkingOptions = null) 24 | { 25 | return new ParallelAsyncLazyForkingPartitionEnumerable(partitioner, threadCount, forkingOptions); 26 | } 27 | 28 | public static IParallelAsyncLazyForkingEnumerable AsAsyncParallel( 29 | this IEnumerable enumerable, 30 | int threadCount, 31 | ForkingOptions? forkingOptions = null) 32 | { 33 | return enumerable switch 34 | { 35 | IReadOnlyList list => new ParallelAsyncLazyForkingRangeEnumerable(list, threadCount, forkingOptions), 36 | _ => new ParallelAsyncLazyForkingPartitionEnumerable(Partitioner.Create(enumerable), threadCount, forkingOptions) 37 | }; 38 | } 39 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Operations/TargetedJoiningTask.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Runtime.CompilerServices; 6 | using AwaitThreading.Core.Context; 7 | using AwaitThreading.Core.Diagnostics; 8 | using AwaitThreading.Core.Tasks; 9 | 10 | namespace AwaitThreading.Core.Operations; 11 | 12 | public readonly struct TargetedJoiningTask 13 | { 14 | public struct TargetedJoiningTaskAwaiter : ICriticalNotifyCompletion, IParallelNotifyCompletion 15 | { 16 | public bool IsCompleted => false; 17 | 18 | public void ParallelOnCompleted(TStateMachine stateMachine) 19 | where TStateMachine : IAsyncStateMachine 20 | { 21 | var context = ParallelContextStorage.PopFrame(); 22 | 23 | if (context.Id == 0) 24 | { 25 | context.JoinBarrier.SignalAndWait(); 26 | stateMachine.MoveNext(); 27 | } 28 | else 29 | { 30 | context.JoinBarrier.Signal(); 31 | ParallelContextStorage.CaptureAndClear(); 32 | } 33 | } 34 | 35 | public void OnCompleted(Action continuation) 36 | { 37 | Assertion.ThrowBadAwait(); 38 | } 39 | 40 | public void UnsafeOnCompleted(Action continuation) 41 | { 42 | Assertion.ThrowBadAwait(); 43 | } 44 | 45 | public void GetResult() 46 | { 47 | } 48 | } 49 | 50 | public TargetedJoiningTaskAwaiter GetAwaiter() => new(); 51 | } -------------------------------------------------------------------------------- /AwaitThreading.Core.Tests/Helpers/BaseClassWithParallelContextValidation.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Collections.Concurrent; 6 | using System.Diagnostics.CodeAnalysis; 7 | using System.Runtime.CompilerServices; 8 | using AwaitThreading.Core.Context; 9 | using AwaitThreading.Core.Diagnostics; 10 | 11 | namespace AwaitThreading.Core.Tests.Helpers; 12 | 13 | public class BaseClassWithParallelContextValidation 14 | { 15 | [TearDown] 16 | public void TearDown() 17 | { 18 | var threadsWithParallelContext = new ConcurrentBag(); 19 | var tasksCount = ThreadPool.ThreadCount; 20 | 21 | var tasks = new Task[tasksCount]; 22 | for (var i = 0; i < tasksCount; i++) 23 | { 24 | tasks[i] = Task.Run( 25 | () => 26 | { 27 | var lastContext = ParallelContextStorage.CaptureAndClear(); 28 | if (!lastContext.IsEmpty) 29 | { 30 | Logger.Log($"Non empty context was detected: {lastContext.StackToString()}"); 31 | threadsWithParallelContext.Add(Thread.CurrentThread.ManagedThreadId); 32 | } 33 | 34 | 35 | Thread.Sleep(10); 36 | }); 37 | } 38 | 39 | Task.WaitAll(tasks); 40 | Assert.That(threadsWithParallelContext, Is.Empty); 41 | } 42 | 43 | [DoesNotReturn] 44 | protected void FailFast([CallerLineNumber] int lineNumber = 0) 45 | { 46 | Logger.Log($"Failing at line number {lineNumber}"); 47 | Environment.Exit(lineNumber); 48 | } 49 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Context/ParallelContextStorage.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2025 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Diagnostics; 6 | using System.Runtime.CompilerServices; 7 | using AwaitThreading.Core.Diagnostics; 8 | 9 | namespace AwaitThreading.Core.Context; 10 | 11 | public static class ParallelContextStorage 12 | { 13 | [field: ThreadStatic] 14 | public static ParallelContext CurrentThreadContext { get; internal set; } 15 | 16 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 17 | public static int GetTopFrameId() => CurrentThreadContext.GetTopFrame().Id; 18 | 19 | internal static ParallelFrame PopFrame() 20 | { 21 | var parallelContext = CurrentThreadContext.PopFrame(out var poppedFrame); 22 | CurrentThreadContext = parallelContext; 23 | return poppedFrame; 24 | } 25 | 26 | internal static ParallelContext CaptureAndClear() 27 | { 28 | var currentContext = CurrentThreadContext; 29 | CurrentThreadContext = default; 30 | Logger.Log("Context cleared"); 31 | return currentContext; 32 | } 33 | 34 | internal static void ClearButNotExpected() 35 | { 36 | VerifyContextIsEmpty(); 37 | CurrentThreadContext = default; 38 | } 39 | 40 | internal static void Restore(ParallelContext context) 41 | { 42 | VerifyContextIsEmpty(); 43 | CurrentThreadContext = context; 44 | Logger.Log("Context restored"); 45 | } 46 | 47 | [Conditional("DEBUG")] 48 | private static void VerifyContextIsEmpty() 49 | { 50 | if (!CurrentThreadContext.IsEmpty) 51 | { 52 | Logger.Log("Context is not empty when expected to be"); 53 | Debug.Fail("Context already exists"); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Tasks/ParallelTaskMethodBuilder.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2023 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Diagnostics; 6 | using System.Runtime.CompilerServices; 7 | using JetBrains.Annotations; 8 | 9 | namespace AwaitThreading.Core.Tasks; 10 | 11 | [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] 12 | public readonly struct ParallelTaskMethodBuilder 13 | { 14 | public ParallelTaskMethodBuilder() 15 | { 16 | } 17 | 18 | public static ParallelTaskMethodBuilder Create() => new(); 19 | 20 | public ParallelTask Task { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } = new(); 21 | 22 | [DebuggerStepThrough] 23 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 24 | public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine 25 | { 26 | // Do not run! Tasks are 'cold' 27 | Task.SetStateMachine(ref stateMachine); 28 | } 29 | 30 | public void SetStateMachine(IAsyncStateMachine stateMachine) 31 | { 32 | } 33 | 34 | public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) 35 | where TAwaiter : INotifyCompletion 36 | where TStateMachine : IAsyncStateMachine 37 | { 38 | ParallelTaskMethodBuilderImpl.AwaitOnCompleted(ref awaiter, ref stateMachine); 39 | } 40 | 41 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 42 | public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) 43 | where TAwaiter : ICriticalNotifyCompletion 44 | where TStateMachine : IAsyncStateMachine 45 | { 46 | ParallelTaskMethodBuilderImpl.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); 47 | } 48 | 49 | public void SetResult() 50 | { 51 | Task.SetResult(); 52 | } 53 | 54 | public void SetException(Exception exception) 55 | { 56 | Task.SetException(exception); 57 | } 58 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Tasks/ParallelTaskMethodBuilder`1.cs: -------------------------------------------------------------------------------- 1 | //MIT License 2 | //Copyright (c) 2023 Saltuk Konstantin 3 | //See the LICENSE file in the project root for more information. 4 | 5 | using System.Diagnostics; 6 | using System.Runtime.CompilerServices; 7 | using JetBrains.Annotations; 8 | 9 | namespace AwaitThreading.Core.Tasks; 10 | 11 | [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] 12 | public readonly struct ParallelTaskMethodBuilder 13 | { 14 | public ParallelTaskMethodBuilder() 15 | { 16 | } 17 | 18 | public static ParallelTaskMethodBuilder Create() => new(); 19 | 20 | public ParallelTask Task { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; } = new(); 21 | 22 | [DebuggerStepThrough] 23 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 24 | public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine 25 | { 26 | // Do not run! Tasks are 'cold' 27 | Task.SetStateMachine(ref stateMachine); 28 | } 29 | 30 | public void SetStateMachine(IAsyncStateMachine stateMachine) 31 | { 32 | } 33 | 34 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 35 | public void AwaitOnCompleted( 36 | ref TAwaiter awaiter, ref TStateMachine stateMachine) 37 | where TAwaiter : INotifyCompletion 38 | where TStateMachine : IAsyncStateMachine 39 | { 40 | ParallelTaskMethodBuilderImpl.AwaitOnCompleted(ref awaiter, ref stateMachine); 41 | } 42 | 43 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 44 | public void AwaitUnsafeOnCompleted( 45 | ref TAwaiter awaiter, ref TStateMachine stateMachine) 46 | where TAwaiter : ICriticalNotifyCompletion 47 | where TStateMachine : IAsyncStateMachine 48 | { 49 | ParallelTaskMethodBuilderImpl.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); 50 | } 51 | 52 | public void SetResult(T result) 53 | { 54 | Task.SetResult(result); 55 | } 56 | 57 | public void SetException(Exception exception) 58 | { 59 | Task.SetException(exception); 60 | } 61 | } -------------------------------------------------------------------------------- /AwaitThreading.Core.Tests/ConcurrencyTests.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using AwaitThreading.Core.Operations; 6 | using AwaitThreading.Core.Tasks; 7 | 8 | namespace AwaitThreading.Core.Tests; 9 | 10 | [TestFixture] 11 | public class ConcurrencyTests : BaseClassWithParallelContextValidation 12 | { 13 | [Test( 14 | Description = @" 15 | UPD: RequireContinuationToBeSetBeforeResult is no longer needed, and now the issue should not be possible, but still check. 16 | Tests that there is no race conditions when tasks inside `MethodThatForks` finish 17 | before the calling methods have set the continuation in the corresponding task. 18 | This test should fail if there is an issue with tracking of `RequireContinuationToBeSetBeforeResult` flag. 19 | For example, test should fail after changing the `ForkingAwaiter` to return `false` 20 | in `RequireContinuationToBeSetBeforeResult`.")] 21 | public async Task NestedOperation_MultipleForks_NoRaceConditions() 22 | { 23 | using var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(10)); 24 | await TestBody().AsTask().WaitAsync(tokenSource.Token); 25 | return; 26 | 27 | async ParallelTask TestBody() 28 | { 29 | for (var i = 0; i < 100; ++i) 30 | { 31 | await MethodThatForks(); 32 | await new JoiningTask(); 33 | } 34 | } 35 | 36 | async ParallelTask MethodThatForks() 37 | { 38 | await new ForkingTask(2); 39 | } 40 | } 41 | 42 | [Test(Description = "Checks that threads are not blocked after finishing own task")] 43 | public async Task Fork_ALotOfThreads_NoStarvation() 44 | { 45 | using var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(1)); 46 | await TestBody().AsTask().WaitAsync(tokenSource.Token); 47 | return; 48 | 49 | async ParallelTask TestBody() 50 | { 51 | await new ForkingTask((Environment.ProcessorCount + 1) * 20); 52 | await new JoiningTask(); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Tasks/ParallelValueTaskMethodBuilder.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2025 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Diagnostics; 6 | using System.Runtime.CompilerServices; 7 | using System.Runtime.ExceptionServices; 8 | using JetBrains.Annotations; 9 | 10 | namespace AwaitThreading.Core.Tasks; 11 | 12 | [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] 13 | public readonly struct ParallelValueTaskMethodBuilder 14 | { 15 | private readonly ParallelTaskImpl _parallelTaskImpl = new(); 16 | 17 | public ParallelValueTaskMethodBuilder() 18 | { 19 | } 20 | 21 | public static ParallelValueTaskMethodBuilder Create() => new(); 22 | 23 | public ParallelValueTask Task => new(_parallelTaskImpl); 24 | 25 | [DebuggerStepThrough] 26 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 27 | public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine 28 | { 29 | _parallelTaskImpl.SetStateMachine(ref stateMachine); 30 | } 31 | 32 | public void SetStateMachine(IAsyncStateMachine stateMachine) 33 | { 34 | } 35 | 36 | public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) 37 | where TAwaiter : INotifyCompletion 38 | where TStateMachine : IAsyncStateMachine 39 | { 40 | ParallelTaskMethodBuilderImpl.AwaitOnCompleted(ref awaiter, ref stateMachine); 41 | } 42 | 43 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 44 | public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) 45 | where TAwaiter : ICriticalNotifyCompletion 46 | where TStateMachine : IAsyncStateMachine 47 | { 48 | ParallelTaskMethodBuilderImpl.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); 49 | } 50 | 51 | public void SetResult() 52 | { 53 | _parallelTaskImpl.SetResult(new ParallelTaskResult(new Unit())); 54 | } 55 | 56 | public void SetException(Exception exception) 57 | { 58 | _parallelTaskImpl.SetResult(new ParallelTaskResult(ExceptionDispatchInfo.Capture(exception))); 59 | } 60 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Tasks/ParallelValueTaskMethodBuilder`1.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Diagnostics; 6 | using System.Runtime.CompilerServices; 7 | using System.Runtime.ExceptionServices; 8 | using JetBrains.Annotations; 9 | 10 | namespace AwaitThreading.Core.Tasks; 11 | 12 | [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] 13 | public readonly struct ParallelValueTaskMethodBuilder 14 | { 15 | private readonly ParallelTaskImpl _parallelTaskImpl = new(); 16 | 17 | public ParallelValueTaskMethodBuilder() 18 | { 19 | } 20 | 21 | public static ParallelValueTaskMethodBuilder Create() => new(); 22 | 23 | public ParallelValueTask Task => new(_parallelTaskImpl); 24 | 25 | [DebuggerStepThrough] 26 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 27 | public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine 28 | { 29 | _parallelTaskImpl.SetStateMachine(ref stateMachine); 30 | } 31 | 32 | public void SetStateMachine(IAsyncStateMachine stateMachine) 33 | { 34 | } 35 | 36 | public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) 37 | where TAwaiter : INotifyCompletion 38 | where TStateMachine : IAsyncStateMachine 39 | { 40 | ParallelTaskMethodBuilderImpl.AwaitOnCompleted(ref awaiter, ref stateMachine); 41 | } 42 | 43 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 44 | public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) 45 | where TAwaiter : ICriticalNotifyCompletion 46 | where TStateMachine : IAsyncStateMachine 47 | { 48 | ParallelTaskMethodBuilderImpl.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); 49 | } 50 | 51 | public void SetResult(T result) 52 | { 53 | _parallelTaskImpl.SetResult(new ParallelTaskResult(result)); 54 | } 55 | 56 | public void SetException(Exception exception) 57 | { 58 | _parallelTaskImpl.SetResult(new ParallelTaskResult(ExceptionDispatchInfo.Capture(exception))); 59 | } 60 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Diagnostics/Assertion.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2023 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Diagnostics; 6 | using System.Diagnostics.CodeAnalysis; 7 | using System.Runtime.CompilerServices; 8 | using System.Runtime.ExceptionServices; 9 | using AwaitThreading.Core.Tasks; 10 | 11 | namespace AwaitThreading.Core.Diagnostics; 12 | 13 | internal static class Assertion 14 | { 15 | private const string? BadAwaitMessage = "Regular async methods do not support forking. Use ParallelTask as a method's return value."; 16 | 17 | public static readonly ExceptionDispatchInfo BadAwaitExceptionDispatchInfo = 18 | ExceptionDispatchInfo.Capture(new InvalidOperationException(BadAwaitMessage)); 19 | 20 | [DoesNotReturn] 21 | public static void StateCorrupted(string message) 22 | { 23 | Debug.Fail(message); 24 | throw new StateCorruptedException(message); 25 | } 26 | 27 | [DoesNotReturn] 28 | public static void ThrowBadAwait() => throw new InvalidOperationException(BadAwaitMessage); 29 | 30 | [DoesNotReturn] 31 | public static void ThrowInvalidTasksCount(int actualCount, [CallerArgumentExpression("actualCount")] string? paramName = null) 32 | { 33 | throw new ArgumentOutOfRangeException(paramName, actualCount, "Fork should have positive number of threads"); 34 | } 35 | 36 | // TODO: mark 'GetResult' methods as Obsolete (considering they are not shown in compiler-generated code)? 37 | [DoesNotReturn] 38 | public static void ThrowInvalidDirectGetResultCall() => throw new NotSupportedException($"Do not call .GetResult() directly on ParallelTask, use .{nameof(ParallelTaskExtensions.AsTask)}().Wait()"); 39 | 40 | [DoesNotReturn] 41 | public static void ThrowInvalidSecondAwaitOfParallelTask() => throw new InvalidOperationException("Parallel task can't be awaited twice"); 42 | 43 | [DoesNotReturn] 44 | public static void ThrowInvalidParallelLocalUsage() => throw new InvalidOperationException("ParallelLocal should be initialized while forking"); 45 | 46 | private class StateCorruptedException : Exception 47 | { 48 | public StateCorruptedException(string message) : base(message) { } 49 | } 50 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Context/ParallelContext.cs: -------------------------------------------------------------------------------- 1 | //MIT License 2 | //Copyright (c) 2023 Saltuk Konstantin 3 | //See the LICENSE file in the project root for more information. 4 | 5 | using System.Collections.Immutable; 6 | using System.Diagnostics.CodeAnalysis; 7 | using System.Diagnostics.Contracts; 8 | using System.Runtime.CompilerServices; 9 | 10 | namespace AwaitThreading.Core.Context; 11 | 12 | public readonly struct ParallelContext : IEquatable 13 | { 14 | private readonly ImmutableStack? _stack; 15 | 16 | private ParallelContext(ImmutableStack stack) 17 | { 18 | _stack = stack; 19 | } 20 | 21 | [MemberNotNullWhen(false, nameof(_stack))] 22 | public bool IsEmpty => _stack is null || _stack.IsEmpty; 23 | 24 | [Pure] 25 | public ParallelFrame GetTopFrame() 26 | { 27 | if (IsEmpty) 28 | { 29 | throw new InvalidOperationException("There are no frames in Parallel Context."); 30 | } 31 | 32 | return _stack.Peek(); 33 | } 34 | 35 | [Pure] 36 | public ParallelContext PushFrame(ParallelFrame frame) 37 | { 38 | var newStack = (_stack ?? ImmutableStack.Empty).Push(frame); 39 | return new ParallelContext(newStack); 40 | } 41 | 42 | [Pure] 43 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 44 | public ParallelContext PopFrame(out ParallelFrame poppedFrame) 45 | { 46 | var currentContextStack = _stack; 47 | if (currentContextStack is null) 48 | { 49 | throw new InvalidOperationException("Stack is empty"); 50 | } 51 | 52 | var newStack = currentContextStack.Pop(out poppedFrame); 53 | return newStack.IsEmpty ? default : new ParallelContext(newStack); 54 | } 55 | 56 | [Pure] 57 | internal string StackToString() 58 | { 59 | return _stack is null 60 | ? "empty" 61 | : string.Join(", ", _stack.Select(t => $"{t.Id}")); 62 | } 63 | 64 | public bool Equals(ParallelContext other) 65 | { 66 | return Equals(_stack, other._stack); 67 | } 68 | 69 | public override bool Equals(object? obj) 70 | { 71 | return obj is ParallelContext other && Equals(other); 72 | } 73 | 74 | public override int GetHashCode() 75 | { 76 | return _stack != null ? _stack.GetHashCode() : 0; 77 | } 78 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Tasks/ParallelValueTaskAwaiter.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2025 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace AwaitThreading.Core.Tasks; 8 | 9 | public readonly struct ParallelValueTaskAwaiter : ICriticalNotifyCompletion, IParallelNotifyCompletion 10 | { 11 | private readonly ParallelValueTask _valueTask; 12 | 13 | internal ParallelValueTaskAwaiter(in ParallelValueTask valueTask) 14 | { 15 | _valueTask = valueTask; 16 | } 17 | 18 | public bool IsCompleted 19 | { 20 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 21 | get => _valueTask.Implementation is null; 22 | } 23 | 24 | public void ParallelOnCompleted(TStateMachine stateMachine) 25 | where TStateMachine : IAsyncStateMachine 26 | { 27 | var implementation = _valueTask.Implementation; 28 | if (implementation == null) 29 | { 30 | // Note: this should not happen within compiler-generated code, 31 | // sync return valueTasks have IsCompleted = true, call MoveNext just to be on a safe side. 32 | stateMachine.MoveNext(); 33 | return; 34 | } 35 | 36 | implementation.ParallelOnCompleted(stateMachine); 37 | } 38 | 39 | public void OnCompleted(Action continuation) 40 | { 41 | var implementation = _valueTask.Implementation; 42 | if (implementation == null) 43 | { 44 | continuation.Invoke(); 45 | return; 46 | } 47 | 48 | implementation.OnCompleted(continuation); 49 | } 50 | 51 | public void UnsafeOnCompleted(Action continuation) 52 | { 53 | var implementation = _valueTask.Implementation; 54 | if (implementation == null) 55 | { 56 | continuation.Invoke(); 57 | return; 58 | } 59 | 60 | implementation.UnsafeOnCompleted(continuation); 61 | } 62 | 63 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 64 | public void GetResult() 65 | { 66 | var implementation = _valueTask.Implementation; 67 | if (implementation == null) 68 | { 69 | return; 70 | } 71 | 72 | var taskResult = implementation.GetResult(); 73 | if (!taskResult.HasResult) 74 | { 75 | taskResult.ExceptionDispatchInfo.Throw(); 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Tasks/ParallelValueTaskAwaiter`1.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace AwaitThreading.Core.Tasks; 8 | 9 | public readonly struct ParallelValueTaskAwaiter : ICriticalNotifyCompletion, IParallelNotifyCompletion 10 | { 11 | private readonly ParallelValueTask _valueTask; 12 | 13 | internal ParallelValueTaskAwaiter(in ParallelValueTask valueTask) 14 | { 15 | _valueTask = valueTask; 16 | } 17 | 18 | public bool IsCompleted 19 | { 20 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 21 | get => _valueTask.Implementation is null; 22 | } 23 | 24 | public void ParallelOnCompleted(TStateMachine stateMachine) 25 | where TStateMachine : IAsyncStateMachine 26 | { 27 | var implementation = _valueTask.Implementation; 28 | if (implementation == null) 29 | { 30 | // Note: this should not happen within compiler-generated code, 31 | // sync return valueTasks have IsCompleted = true, call MoveNext just to be on a safe side. 32 | stateMachine.MoveNext(); 33 | return; 34 | } 35 | 36 | implementation.ParallelOnCompleted(stateMachine); 37 | } 38 | 39 | public void OnCompleted(Action continuation) 40 | { 41 | var implementation = _valueTask.Implementation; 42 | if (implementation == null) 43 | { 44 | continuation.Invoke(); 45 | return; 46 | } 47 | 48 | implementation.OnCompleted(continuation); 49 | } 50 | 51 | public void UnsafeOnCompleted(Action continuation) 52 | { 53 | var implementation = _valueTask.Implementation; 54 | if (implementation == null) 55 | { 56 | continuation.Invoke(); 57 | return; 58 | } 59 | 60 | implementation.UnsafeOnCompleted(continuation); 61 | } 62 | 63 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 64 | public T GetResult() 65 | { 66 | var implementation = _valueTask.Implementation; 67 | if (implementation == null) 68 | { 69 | return _valueTask.Result!; 70 | } 71 | 72 | var taskResult = implementation.GetResult(); 73 | if (!taskResult.HasResult) 74 | { 75 | taskResult.ExceptionDispatchInfo.Throw(); 76 | } 77 | 78 | return taskResult.Result; 79 | } 80 | } -------------------------------------------------------------------------------- /Samples/Program.cs: -------------------------------------------------------------------------------- 1 | using AwaitThreading.Core; 2 | using AwaitThreading.Core.Operations; 3 | using AwaitThreading.Core.Tasks; 4 | using AwaitThreading.Enumerable; 5 | using AwaitThreading.Enumerable.Experimental; 6 | 7 | await NormalForkAndJoin(5); 8 | await CompositionExample(5); 9 | await CustomOptions(); 10 | 11 | await AsParallelAsync(); 12 | await AsParallel(); 13 | await AsParallelExperimental(); 14 | 15 | async ParallelTask CompositionExample(int threadCount) 16 | { 17 | var id = await ForkAndGetId(threadCount); 18 | Console.Out.WriteLine($"Hello world from {id}"); 19 | await JoinInsideMethod(); 20 | } 21 | 22 | async ParallelTask ForkAndGetId(int threadCount) 23 | { 24 | var id = await ParallelOperations.Fork(threadCount); 25 | return id; 26 | } 27 | 28 | async ParallelTask JoinInsideMethod() 29 | { 30 | await ParallelOperations.Join(); 31 | } 32 | 33 | 34 | async ParallelTask NormalForkAndJoin(int threadCount) 35 | { 36 | Console.Out.WriteLine("Before fork: single thread"); 37 | 38 | var id = await ParallelOperations.Fork(threadCount); 39 | Console.Out.WriteLine($"Hello world from {id}"); //executed on two different threads 40 | 41 | // any (sync or async) workload 42 | await Task.Delay(100); 43 | Thread.Sleep(100); 44 | 45 | await ParallelOperations.Join(); 46 | Console.Out.WriteLine("After join: single thread"); 47 | } 48 | 49 | async ParallelTask CustomOptions() 50 | { 51 | var id = await ParallelOperations.Fork(2, new ForkingOptions{TaskCreationOptions = TaskCreationOptions.PreferFairness, TaskScheduler = TaskScheduler.Default}); 52 | Console.Out.WriteLine($"Hello world from {id}"); 53 | await ParallelOperations.JoinOnMainThread(); 54 | } 55 | 56 | async ParallelTask AsParallelAsync() 57 | { 58 | var list = Enumerable.Range(1, 10).ToList(); 59 | await foreach (var item in await list.AsParallelAsync(3)) 60 | { 61 | Console.Out.WriteLine($"Processing element {item}"); 62 | await Task.Delay(10); //simulate some workload 63 | } 64 | } 65 | 66 | async ParallelTask AsParallel() 67 | { 68 | var list = Enumerable.Range(1, 10).ToList(); 69 | await foreach (var item in list.AsAsyncParallel(3)) 70 | { 71 | Console.Out.WriteLine($"Processing element {item}"); 72 | await Task.Delay(10); //simulate some workload 73 | } 74 | } 75 | 76 | async ParallelTask AsParallelExperimental() 77 | { 78 | var list = Enumerable.Range(1, 10).ToList(); 79 | await foreach (var item in list) 80 | { 81 | Console.Out.WriteLine($"Processing element {item}"); 82 | await Task.Delay(10); //simulate some workload 83 | } 84 | } -------------------------------------------------------------------------------- /AwaitThreading.Enumerable/ParallelAsyncLazyForkingPartitionEnumerator.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Collections.Concurrent; 6 | using AwaitThreading.Core; 7 | using AwaitThreading.Core.Context; 8 | using AwaitThreading.Core.Operations; 9 | using AwaitThreading.Core.Tasks; 10 | 11 | namespace AwaitThreading.Enumerable; 12 | 13 | public readonly struct ParallelAsyncLazyForkingPartitionEnumerator : IParallelAsyncLazyForkingEnumerator 14 | { 15 | private readonly Partitioner _partitioner; 16 | private readonly int _threadCount; 17 | private readonly ForkingOptions? _forkingOptions; 18 | 19 | // In ideal world we would be able to store enumerator for our chunk in struct field, 20 | // but any changes to the state of this struct will be lost since async methods are 21 | // executed on the copy of a struct, so we have to store the data somewhere else. 22 | private readonly ParallelLocal> _chunkIndexer = new(); 23 | 24 | public ParallelAsyncLazyForkingPartitionEnumerator(Partitioner partitioner, int threadCount, ForkingOptions? forkingOptions) 25 | { 26 | _partitioner = partitioner; 27 | _threadCount = threadCount; 28 | _forkingOptions = forkingOptions; 29 | } 30 | 31 | public ParallelValueTask MoveNextAsync() 32 | { 33 | if (_chunkIndexer.IsInitialized) 34 | { 35 | return ParallelValueTask.FromResult(_chunkIndexer.Value!.MoveNext()); 36 | } 37 | 38 | return ForkAndMoveNextAsync(); 39 | } 40 | 41 | private async ParallelValueTask ForkAndMoveNextAsync() 42 | { 43 | var partitioner = _partitioner; 44 | if (partitioner.SupportsDynamicPartitions) 45 | { 46 | var dynamicPartitions = partitioner.GetDynamicPartitions(); 47 | await _chunkIndexer.InitializeAndFork(_threadCount, _forkingOptions); 48 | 49 | // ReSharper disable once GenericEnumeratorNotDisposed 50 | var dynamicEnumerator = dynamicPartitions.GetEnumerator(); 51 | _chunkIndexer.Value = dynamicEnumerator; 52 | return dynamicEnumerator.MoveNext(); 53 | } 54 | 55 | var partitions = partitioner.GetPartitions(_threadCount); 56 | await _chunkIndexer.InitializeAndFork(_threadCount, _forkingOptions); 57 | 58 | var enumerator = partitions[ParallelContextStorage.GetTopFrameId()]; 59 | _chunkIndexer.Value = enumerator; 60 | return enumerator.MoveNext(); 61 | } 62 | 63 | public T Current => _chunkIndexer.Value!.Current; 64 | 65 | public async ParallelTask DisposeAsync() 66 | { 67 | if (_chunkIndexer.IsInitialized) 68 | { 69 | _chunkIndexer.Value!.Dispose(); 70 | await new JoiningTask(); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /AwaitThreading.Enumerable/CollectionParallelExtensions.ParallelAsync.cs: -------------------------------------------------------------------------------- 1 | //MIT License 2 | //Copyright (c) 2023 Saltuk Konstantin 3 | //See the LICENSE file in the project root for more information. 4 | 5 | using System.Collections.Concurrent; 6 | using AwaitThreading.Core.Context; 7 | using AwaitThreading.Core.Operations; 8 | using AwaitThreading.Core.Tasks; 9 | 10 | namespace AwaitThreading.Enumerable; 11 | 12 | public static partial class CollectionParallelExtensions 13 | { 14 | public static async ParallelTask> AsParallelAsync( 15 | this IReadOnlyList list, 16 | int threadCount, 17 | ForkingOptions? forkingOptions = null) 18 | { 19 | var rangeManager = new RangeManager(0, list.Count, 1, threadCount); 20 | await new ForkingTask(threadCount, forkingOptions); 21 | return new ParallelAsyncEnumerable(list, rangeManager.RegisterNewWorker()); 22 | } 23 | 24 | public static async ParallelTask> AsParallelAsync( 25 | this Partitioner partitioner, 26 | int threadCount, 27 | ForkingOptions? forkingOptions = null) 28 | { 29 | var forked = false; 30 | try 31 | { 32 | if (partitioner.SupportsDynamicPartitions) 33 | { 34 | var dynamicPartitions = partitioner.GetDynamicPartitions(); 35 | await new ForkingTask(threadCount, forkingOptions); 36 | forked = true; 37 | 38 | return new ParallelAsyncDelegatingEnumerable(dynamicPartitions.GetEnumerator()); 39 | } 40 | 41 | var partitions = partitioner.GetPartitions(threadCount); 42 | await new ForkingTask(threadCount, forkingOptions); 43 | forked = true; 44 | 45 | return new ParallelAsyncDelegatingEnumerable(partitions[ParallelContextStorage.GetTopFrameId()]); 46 | } 47 | catch 48 | { 49 | if (forked) 50 | { 51 | await new JoiningTask(); 52 | } 53 | 54 | throw; 55 | } 56 | } 57 | 58 | public static ParallelTask> AsParallelAsync( 59 | this IEnumerable enumerable, 60 | int threadCount, 61 | ForkingOptions? forkingOptions = null) 62 | { 63 | return enumerable switch 64 | { 65 | IReadOnlyList list => AsParallelAsyncBoxed(list, threadCount, forkingOptions), 66 | _ => AsParallelAsync(Partitioner.Create(enumerable), threadCount) 67 | }; 68 | } 69 | 70 | private static async ParallelTask> AsParallelAsyncBoxed( 71 | IReadOnlyList list, 72 | int threadCount, 73 | ForkingOptions? forkingOptions) 74 | { 75 | var rangeManager = new RangeManager(0, list.Count, 1, threadCount); 76 | await new ForkingTask(threadCount, forkingOptions); 77 | return new ParallelAsyncEnumerable(list, rangeManager.RegisterNewWorker()); 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /AwaitThreading.Enumerable/ParallelAsyncLazyForkingRangeEnumerator.cs: -------------------------------------------------------------------------------- 1 | //MIT License 2 | //Copyright (c) 2023 Saltuk Konstantin 3 | //See the LICENSE file in the project root for more information. 4 | 5 | using AwaitThreading.Core; 6 | using AwaitThreading.Core.Operations; 7 | using AwaitThreading.Core.Tasks; 8 | 9 | namespace AwaitThreading.Enumerable; 10 | 11 | public readonly struct ParallelAsyncLazyForkingRangeEnumerator : IParallelAsyncLazyForkingEnumerator 12 | { 13 | private class RangeEnumerator 14 | { 15 | private RangeWorker _rangeWorker; 16 | private int _fromInclusive; 17 | private int _toExclusive; 18 | 19 | public RangeEnumerator(RangeWorker rangeWorker) 20 | { 21 | _rangeWorker = rangeWorker; 22 | } 23 | 24 | public bool MoveNext() 25 | { 26 | if (_fromInclusive++ >= _toExclusive - 1) 27 | { 28 | return _rangeWorker.FindNewWork(out _fromInclusive, out _toExclusive); 29 | } 30 | 31 | return true; 32 | } 33 | 34 | public T GetItem(IReadOnlyList list) => list[_fromInclusive]; 35 | } 36 | 37 | private readonly IReadOnlyList _list; 38 | private readonly int _threadCount; 39 | private readonly ForkingOptions? _forkingOptions; 40 | 41 | // In ideal world we would be able to store enumerator for our chunk in struct field, 42 | // but any changes to the state of this struct will be lost since async methods are 43 | // executed on the copy of a struct, so we have to store the data somewhere else. 44 | private readonly ParallelLocal _chunkIndexer = new(); 45 | 46 | public ParallelAsyncLazyForkingRangeEnumerator(IReadOnlyList list, int threadCount, ForkingOptions? forkingOptions) 47 | { 48 | _threadCount = threadCount; 49 | _forkingOptions = forkingOptions; 50 | _list = list; 51 | } 52 | 53 | public ParallelValueTask MoveNextAsync() 54 | { 55 | if (_chunkIndexer.IsInitialized) 56 | { 57 | return ParallelValueTask.FromResult(_chunkIndexer.Value!.MoveNext()); 58 | } 59 | 60 | if (_list.Count == 0) 61 | { 62 | return ParallelValueTask.FromResult(false); 63 | } 64 | 65 | return ForkAndMoveNextAsync(); 66 | } 67 | 68 | private async ParallelValueTask ForkAndMoveNextAsync() 69 | { 70 | var rangeManager = new RangeManager(0, _list.Count, 1, _threadCount); 71 | await _chunkIndexer.InitializeAndFork(_threadCount, _forkingOptions); 72 | var indexer = new RangeEnumerator(rangeManager.RegisterNewWorker()); 73 | var returnResult = indexer.MoveNext(); 74 | _chunkIndexer.Value = indexer; 75 | return returnResult; 76 | } 77 | 78 | public T Current => _chunkIndexer.Value!.GetItem(_list); 79 | 80 | public async ParallelTask DisposeAsync() 81 | { 82 | if (_chunkIndexer.IsInitialized) 83 | { 84 | await new JoiningTask(); 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /AwaitThreading.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AwaitThreading.Core", "AwaitThreading.Core\AwaitThreading.Core.csproj", "{C8B3B39E-3F96-4282-8807-890E39E5EA3E}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AwaitThreading.Enumerable", "AwaitThreading.Enumerable\AwaitThreading.Enumerable.csproj", "{8584C4C2-609B-4EDA-B09E-4C72FADFC608}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples", "Samples\Samples.csproj", "{570AFD1C-86BE-404B-B0A2-8182920FFFBB}" 8 | EndProject 9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AwaitThreading.Core.Tests", "AwaitThreading.Core.Tests\AwaitThreading.Core.Tests.csproj", "{AA2E4F51-9A18-4245-AD29-AFE02C8D0CE5}" 10 | EndProject 11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "Benchmarks\Benchmarks.csproj", "{BA044382-7DA0-4EEE-AB5C-3D2D901F6841}" 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AwaitThreading.Enumerable.Tests", "AwaitThreading.Enumerable.Tests\AwaitThreading.Enumerable.Tests.csproj", "{1ED3CB66-2427-4DA0-B847-EFD5C2065A15}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {C8B3B39E-3F96-4282-8807-890E39E5EA3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {C8B3B39E-3F96-4282-8807-890E39E5EA3E}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {C8B3B39E-3F96-4282-8807-890E39E5EA3E}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {C8B3B39E-3F96-4282-8807-890E39E5EA3E}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {8584C4C2-609B-4EDA-B09E-4C72FADFC608}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {8584C4C2-609B-4EDA-B09E-4C72FADFC608}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {8584C4C2-609B-4EDA-B09E-4C72FADFC608}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {8584C4C2-609B-4EDA-B09E-4C72FADFC608}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {570AFD1C-86BE-404B-B0A2-8182920FFFBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {570AFD1C-86BE-404B-B0A2-8182920FFFBB}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {570AFD1C-86BE-404B-B0A2-8182920FFFBB}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {570AFD1C-86BE-404B-B0A2-8182920FFFBB}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {AA2E4F51-9A18-4245-AD29-AFE02C8D0CE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {AA2E4F51-9A18-4245-AD29-AFE02C8D0CE5}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {AA2E4F51-9A18-4245-AD29-AFE02C8D0CE5}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {AA2E4F51-9A18-4245-AD29-AFE02C8D0CE5}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {BA044382-7DA0-4EEE-AB5C-3D2D901F6841}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {BA044382-7DA0-4EEE-AB5C-3D2D901F6841}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {BA044382-7DA0-4EEE-AB5C-3D2D901F6841}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {BA044382-7DA0-4EEE-AB5C-3D2D901F6841}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {1ED3CB66-2427-4DA0-B847-EFD5C2065A15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {1ED3CB66-2427-4DA0-B847-EFD5C2065A15}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {1ED3CB66-2427-4DA0-B847-EFD5C2065A15}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {1ED3CB66-2427-4DA0-B847-EFD5C2065A15}.Release|Any CPU.Build.0 = Release|Any CPU 45 | EndGlobalSection 46 | EndGlobal 47 | -------------------------------------------------------------------------------- /AwaitThreading.Core/Tasks/ParallelTaskMethodBuilderImpl.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2023 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Runtime.CompilerServices; 6 | using AwaitThreading.Core.Context; 7 | 8 | namespace AwaitThreading.Core.Tasks; 9 | 10 | internal static class ParallelTaskMethodBuilderImpl 11 | { 12 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 13 | public static void AwaitOnCompleted( 14 | ref TAwaiter awaiter, ref TStateMachine stateMachine) 15 | where TAwaiter : INotifyCompletion 16 | where TStateMachine : IAsyncStateMachine 17 | { 18 | if (awaiter is IParallelNotifyCompletion parallelAwaiter) 19 | { 20 | AwaitParallelOnCompletedInternal(ref parallelAwaiter, stateMachine); 21 | } 22 | else 23 | { 24 | AwaitOnCompletedInternal(ref awaiter, stateMachine); 25 | } 26 | } 27 | 28 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 29 | public static void AwaitUnsafeOnCompleted( 30 | ref TAwaiter awaiter, ref TStateMachine stateMachine) 31 | where TAwaiter : ICriticalNotifyCompletion 32 | where TStateMachine : IAsyncStateMachine 33 | { 34 | if (awaiter is IParallelNotifyCompletion parallelAwaiter) 35 | { 36 | AwaitParallelOnCompletedInternal(ref parallelAwaiter, stateMachine); 37 | } 38 | else 39 | { 40 | AwaitUnsafeOnCompletedInternal(ref awaiter, stateMachine); 41 | } 42 | } 43 | 44 | private static void AwaitParallelOnCompletedInternal( 45 | ref TAwaiter parallelAwaiter, 46 | TStateMachine stateMachine) 47 | where TAwaiter : IParallelNotifyCompletion 48 | where TStateMachine : IAsyncStateMachine 49 | { 50 | parallelAwaiter.ParallelOnCompleted(stateMachine); 51 | } 52 | 53 | private static void AwaitOnCompletedInternal( 54 | ref TAwaiter awaiter, 55 | TStateMachine stateMachine) 56 | where TAwaiter : INotifyCompletion 57 | where TStateMachine : IAsyncStateMachine 58 | { 59 | var executionContext = ExecutionContext.Capture(); 60 | 61 | if (executionContext is null) 62 | { 63 | awaiter.OnCompleted(() => { stateMachine.MoveNext(); }); 64 | } 65 | else 66 | { 67 | awaiter.OnCompleted(() => 68 | { 69 | ExecutionContext.Restore(executionContext); //TODO: custom closure class instead of lambda 70 | stateMachine.MoveNext(); 71 | }); 72 | } 73 | } 74 | 75 | private static void AwaitUnsafeOnCompletedInternal( 76 | ref TAwaiter awaiter, 77 | TStateMachine stateMachine) 78 | where TAwaiter : ICriticalNotifyCompletion 79 | where TStateMachine : IAsyncStateMachine 80 | { 81 | var executionContext = ExecutionContext.Capture(); 82 | var parallelContext = ParallelContextStorage.CaptureAndClear(); 83 | 84 | if (executionContext is null) 85 | { 86 | awaiter.UnsafeOnCompleted(() => 87 | { 88 | ParallelContextStorage.Restore(parallelContext); 89 | stateMachine.MoveNext(); 90 | ParallelContextStorage.ClearButNotExpected(); 91 | }); 92 | } 93 | else 94 | { 95 | awaiter.UnsafeOnCompleted(() => 96 | { 97 | ExecutionContext.Restore(executionContext); 98 | ParallelContextStorage.Restore(parallelContext); 99 | stateMachine.MoveNext(); 100 | ParallelContextStorage.ClearButNotExpected(); 101 | }); 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Operations/ForkingTask.cs: -------------------------------------------------------------------------------- 1 | //MIT License 2 | //Copyright (c) 2023 Saltuk Konstantin 3 | //See the LICENSE file in the project root for more information. 4 | 5 | using System.Runtime.CompilerServices; 6 | using AwaitThreading.Core.Context; 7 | using AwaitThreading.Core.Diagnostics; 8 | using AwaitThreading.Core.Tasks; 9 | 10 | namespace AwaitThreading.Core.Operations; 11 | 12 | public readonly struct ForkingTask 13 | { 14 | public readonly struct ForkingAwaiter : ICriticalNotifyCompletion, IParallelNotifyCompletion 15 | { 16 | private readonly int _threadCount; 17 | private readonly ForkingOptions? _options; 18 | 19 | public ForkingAwaiter(int threadCount, ForkingOptions? options) 20 | { 21 | _threadCount = threadCount; 22 | _options = options; 23 | } 24 | 25 | public bool IsCompleted => false; 26 | 27 | public void ParallelOnCompleted(TStateMachine stateMachine) 28 | where TStateMachine : IAsyncStateMachine 29 | { 30 | var forkingClosure = new ForkingClosure( 31 | stateMachine, 32 | _threadCount, 33 | ParallelContextStorage.CaptureAndClear()); 34 | 35 | for (var i = 0; i < _threadCount; ++i) 36 | { 37 | Logger.Log("Scheduling task " + i); 38 | Task.Factory.StartNew( 39 | static args => 40 | { 41 | try 42 | { 43 | ((ForkingClosure)args!).StartNewThread(); 44 | } 45 | finally 46 | { 47 | ParallelContextStorage.ClearButNotExpected(); 48 | } 49 | }, 50 | forkingClosure, 51 | CancellationToken.None, 52 | _options?.TaskCreationOptions ?? TaskCreationOptions.None, 53 | _options?.TaskScheduler ?? TaskScheduler.Default 54 | ); 55 | } 56 | } 57 | 58 | public void OnCompleted(Action continuation) 59 | { 60 | Assertion.ThrowBadAwait(); 61 | } 62 | 63 | public void UnsafeOnCompleted(Action continuation) 64 | { 65 | Assertion.ThrowBadAwait(); 66 | } 67 | 68 | public void GetResult() 69 | { 70 | } 71 | } 72 | 73 | private readonly ForkingAwaiter _awaiter; 74 | 75 | public ForkingTask(int threadCount, ForkingOptions? options = null) 76 | { 77 | if (threadCount <= 0) 78 | Assertion.ThrowInvalidTasksCount(threadCount); 79 | 80 | _awaiter = new ForkingAwaiter(threadCount, options); 81 | } 82 | 83 | public ForkingAwaiter GetAwaiter() => _awaiter; 84 | } 85 | 86 | public class ForkingClosure 87 | where TStateMachine : IAsyncStateMachine 88 | { 89 | private readonly TStateMachine _stateMachine; 90 | private readonly ExecutionContext? _executionContext; 91 | private readonly SingleWaiterBarrier _barrier; 92 | private readonly ParallelContext _parallelContext; 93 | private int _myThreadId = -1; 94 | 95 | public ForkingClosure(TStateMachine stateMachine, int threadCount, ParallelContext parallelContext) 96 | { 97 | _executionContext = ExecutionContext.Capture(); 98 | _stateMachine = stateMachine; 99 | _parallelContext = parallelContext; 100 | _barrier = new SingleWaiterBarrier(threadCount); 101 | } 102 | 103 | public void StartNewThread() 104 | { 105 | if (_executionContext is not null) 106 | { 107 | ExecutionContext.Restore(_executionContext); 108 | } 109 | 110 | var newFrame = new ParallelFrame(Interlocked.Increment(ref _myThreadId), _barrier); 111 | ParallelContextStorage.CurrentThreadContext = _parallelContext.PushFrame(newFrame); 112 | Logger.Log("Task started"); 113 | _stateMachine.MakeCopy().MoveNext(); 114 | } 115 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AwaitThreading 2 | 3 | AwaitThreading is a project dedicated to ease the use of parallel programming using async/await infrastructure of C# programming language. 4 | 5 | ## Structure 6 | The project consist of two main parts: 7 | 8 | - **AwaitThreading.Core:** This is where the core functionality of the Fork-join model is implemented. It includes `ForkTask` and `JoinTask` that provide the functionality of forking and joining using `await` operator. It utilizes compiler-generated state machine to run parts of async method in parallel. All operations are also available via `ParallelOperations` class. There are also some helper entities (like `ParallelLocal` or `ParallelContextStorage` with `ParallelContext`). 9 | 10 | - Some applications of this concept 11 | - **AwaitThreading.Enumerable:** Contains extension methods for collections, enabling parallel iteration over collection using a simple foreach loop. 12 | - **To be added:** MPI-like interface, util classes for auto fork\join with `using` construction, etc. 13 | 14 | ### AwaitThreading.Core 15 | 16 | Basic example: 17 | ```csharp 18 | using AwaitThreading.Core; 19 | using AwaitThreading.Core.Tasks; 20 | 21 | await NormalForkAndJoin(5); 22 | 23 | async ParallelTask NormalForkAndJoin(int threadCount) 24 | { 25 | Console.Out.WriteLine("Before fork: single thread"); 26 | 27 | var id = await ParallelOperations.Fork(threadCount); 28 | Console.Out.WriteLine($"Hello world from {id}"); //executed on two different threads 29 | 30 | // any (sync or async) workload 31 | await Task.Delay(100); 32 | Thread.Sleep(100); 33 | 34 | await ParallelOperations.Join(); 35 | Console.Out.WriteLine("After join: single thread"); 36 | } 37 | ``` 38 | 39 | Methods composition: 40 | ```csharp 41 | using AwaitThreading.Core; 42 | using AwaitThreading.Core.Tasks; 43 | 44 | await CompositionExample(5); 45 | 46 | async ParallelTask CompositionExample(int threadCount) 47 | { 48 | var id = await ForkAndGetId(threadCount); 49 | Console.Out.WriteLine($"Hello world from {id}"); 50 | await JoinInsideMethod(); 51 | } 52 | 53 | async ParallelTask ForkAndGetId(int threadCount) 54 | { 55 | var id = await ParallelOperations.Fork(threadCount); 56 | return id; 57 | } 58 | 59 | async ParallelTask JoinInsideMethod() 60 | { 61 | await ParallelOperations.Join(); 62 | } 63 | ``` 64 | 65 | ### AwaitThreading.Enumerable 66 | 67 | There are two main methods: `AsParallel` and `AsParallelAsync`. The key difference is that `AsParallelAsync` performs forking inside it, so after `AsParallelAsync` call caller is already forked, and it requires additional `await` to perform this operation.`AsParallel`, on the other hand, returns `ParallelLazyAsyncEnumerable` and the fork happens inside `ParallelLazyAsyncEnumerator.MoveNextAsync` on the first foreach iteration. This allows caller to not write additional `await` but has slightly more overhead. 68 | ```csharp 69 | using AwaitThreading.Core.Tasks; 70 | using AwaitThreading.Enumerable; 71 | 72 | await AsParallelAsync(); 73 | await AsParallel(); 74 | 75 | async ParallelTask AsParallelAsync() 76 | { 77 | var list = Enumerable.Range(1, 10).ToList(); 78 | await foreach (var item in await list.AsParallelAsync(3)) 79 | { 80 | // foreach body is executed across three separate threads 81 | Console.Out.WriteLine($"Processing element {item}"); 82 | await Task.Delay(10); //simulate some workload 83 | } 84 | } 85 | 86 | async ParallelTask AsParallel() 87 | { 88 | var list = Enumerable.Range(1, 10).ToList(); 89 | await foreach (var item in list.AsAsyncParallel(3)) 90 | { 91 | // foreach body is executed across three separate threads 92 | Console.Out.WriteLine($"Processing element {item}"); 93 | await Task.Delay(10); //simulate some workload 94 | } 95 | } 96 | ``` 97 | 98 | ## Project status 99 | Project is in development state and not production-ready yet. 100 | 101 | TODO list of critical items: 102 | - API is not finalized and provides data to some internal structures (like `ParallelContext`) 103 | - ExecutionContext is not always restored when it has to 104 | 105 | ## Known limitations 106 | - Exceptions are not propagated from parallel foreach body (compiler-generated state machine saves the exception to a field and rethrows this after `DisposeAsync()`, so there is no way to retrieve this exception for now) -------------------------------------------------------------------------------- /AwaitThreading.Core/ParallelOperations.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2025 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Runtime.CompilerServices; 6 | using AwaitThreading.Core.Context; 7 | using AwaitThreading.Core.Operations; 8 | using AwaitThreading.Core.Tasks; 9 | 10 | namespace AwaitThreading.Core; 11 | 12 | public static class ParallelOperations 13 | { 14 | /// 15 | /// Returns a task that performs Fork operation while awaiting. 16 | /// After the Fork operation, threads will execute 17 | /// the method starting from the next line. Forks support nesting, e.g. each forked thread 18 | /// can also perform Fork operations. 19 | /// 20 | /// Number of threads for Fork operation 21 | /// Additional options to configure how executor tasks are starting 22 | /// is zero or negative 23 | /// Return type of the calling method is not , or -alternative. This exception is thrown during the `await` operation. 24 | /// The id of current thread, from 0 to ( - 1) inclusive 25 | /// 26 | /// 27 | /// async ParallelTask MyAsyncMethod() 28 | /// { 29 | /// var id = await ParallelOperations.Fork(2); 30 | /// Console.Out.WriteLine($"Hello World from thread {id}"); 31 | /// await Task.Delay(100); // any async workload 32 | /// Thread.Sleep(100); //any sync workload 33 | /// await ParallelOperations.Join(); 34 | /// } 35 | /// 36 | /// 37 | public static ForkingTaskWithId Fork(int threadCount, ForkingOptions? forkingOptions = null) 38 | { 39 | return new ForkingTaskWithId(new ForkingTask(threadCount, forkingOptions)); 40 | } 41 | 42 | /// 43 | /// Returns a task that performs Join operation while awaiting. 44 | /// One of the previously forked threads will proceed after the Join operation. 45 | /// 46 | /// Threads are not currently forked, e.g. is empty. This exception is thrown during the `await` operation. 47 | public static JoiningTask Join() 48 | { 49 | return new JoiningTask(); 50 | } 51 | 52 | /// 53 | /// Returns a task that performs Join operation while awaiting. 54 | /// Thread with ID=0 will proceed after the Join operation. 55 | /// 56 | /// Threads are not currently forked, e.g. is empty. This exception is thrown during the `await` operation. 57 | public static TargetedJoiningTask JoinOnMainThread() 58 | { 59 | return new TargetedJoiningTask(); 60 | } 61 | } 62 | 63 | public readonly struct ForkingTaskWithId 64 | { 65 | private readonly ForkingTask _forkingTask; 66 | 67 | public ForkingTaskWithId(ForkingTask forkingTask) 68 | { 69 | _forkingTask = forkingTask; 70 | } 71 | 72 | public ForkingAwaiterWithId GetAwaiter() => new(_forkingTask.GetAwaiter()); 73 | 74 | public readonly struct ForkingAwaiterWithId : ICriticalNotifyCompletion, IParallelNotifyCompletion 75 | { 76 | private readonly ForkingTask.ForkingAwaiter _forkingAwaiter; 77 | 78 | public ForkingAwaiterWithId(ForkingTask.ForkingAwaiter forkingAwaiter) 79 | { 80 | _forkingAwaiter = forkingAwaiter; 81 | } 82 | 83 | public bool IsCompleted => _forkingAwaiter.IsCompleted; 84 | 85 | public void ParallelOnCompleted(TStateMachine stateMachine) 86 | where TStateMachine : IAsyncStateMachine 87 | { 88 | _forkingAwaiter.ParallelOnCompleted(stateMachine); 89 | } 90 | 91 | public void OnCompleted(Action continuation) 92 | { 93 | _forkingAwaiter.OnCompleted(continuation); 94 | } 95 | 96 | public void UnsafeOnCompleted(Action continuation) 97 | { 98 | _forkingAwaiter.UnsafeOnCompleted(continuation); 99 | } 100 | 101 | public int GetResult() 102 | { 103 | return ParallelContextStorage.GetTopFrameId(); 104 | } 105 | } 106 | 107 | } -------------------------------------------------------------------------------- /AwaitThreading.Core.Tests/Tasks/ParallelTaskExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2025 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using AwaitThreading.Core.Tasks; 6 | #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously 7 | 8 | namespace AwaitThreading.Core.Tests.Tasks; 9 | 10 | [TestFixture] 11 | [TestOf(typeof(ParallelTaskExtensions))] 12 | public class ParallelTaskExtensionsTests 13 | { 14 | [Test] 15 | public async Task AsTask_GetSyncResult_ReturnsResult() 16 | { 17 | var result = await ParallelMethod().AsTask(); 18 | Assert.That(result, Is.EqualTo(42)); 19 | 20 | async ParallelTask ParallelMethod() 21 | { 22 | return 42; 23 | } 24 | } 25 | 26 | [Test] 27 | public async Task AsTask_GetAsyncResult_ReturnsResult() 28 | { 29 | var result = await ParallelMethod().AsTask(); 30 | Assert.That(result, Is.EqualTo(42)); 31 | 32 | async ParallelTask ParallelMethod() 33 | { 34 | await Task.Yield(); 35 | return 42; 36 | } 37 | } 38 | 39 | [Test] 40 | public async Task AsTask_GetAsyncResultAfterParallelOperations_ReturnsResult() 41 | { 42 | var result = await ParallelMethod().AsTask(); 43 | Assert.That(result, Is.EqualTo(42)); 44 | 45 | async ParallelTask ParallelMethod() 46 | { 47 | await ParallelOperations.Fork(2); 48 | await ParallelOperations.Join(); 49 | return 42; 50 | } 51 | } 52 | 53 | [Test] 54 | public async Task AsTask_GetAsyncResult_ExceptionRethrown() 55 | { 56 | await AssertEx.CheckThrowsAsync(async () => await ParallelMethod().AsTask()); 57 | 58 | async ParallelTask ParallelMethod() 59 | { 60 | await Task.Yield(); 61 | throw new ArgumentOutOfRangeException(); 62 | } 63 | } 64 | 65 | [Test] 66 | public async Task AsTask_GetAsyncResultAfterParallelOperations_ExceptionRethrown() 67 | { 68 | await AssertEx.CheckThrowsAsync(async () => await ParallelMethod().AsTask()); 69 | 70 | async ParallelTask ParallelMethod() 71 | { 72 | await ParallelOperations.Fork(2); 73 | await ParallelOperations.Join(); 74 | throw new ArgumentOutOfRangeException(); 75 | } 76 | } 77 | 78 | [TestCase(true)] 79 | [TestCase(false)] 80 | public async Task AsTask_WaitSynchronouslyForAsyncResult_ReturnsResult(bool useParallel) 81 | { 82 | Assert.That(ParallelMethod().AsTask().Result, Is.EqualTo(42)); 83 | 84 | async ParallelTask ParallelMethod() 85 | { 86 | if (useParallel) 87 | { 88 | await ParallelOperations.Fork(2); 89 | await ParallelOperations.Join(); 90 | } 91 | else 92 | { 93 | await Task.Yield(); 94 | } 95 | 96 | return 42; 97 | } 98 | } 99 | 100 | [TestCase(true)] 101 | [TestCase(false)] 102 | public async Task AsValueTask_WaitSynchronouslyForAsyncResult_ReturnsResult(bool useParallel) 103 | { 104 | Assert.That(ParallelMethod().AsValueTask().Result, Is.EqualTo(42)); 105 | 106 | async ParallelValueTask ParallelMethod() 107 | { 108 | if (useParallel) 109 | { 110 | await ParallelOperations.Fork(2); 111 | await ParallelOperations.Join(); 112 | } 113 | else 114 | { 115 | await Task.Yield(); 116 | } 117 | 118 | return 42; 119 | } 120 | } 121 | 122 | [Test] 123 | public async Task AsValueTask_GetSyncResult_ReturnsResultSynchronously() 124 | { 125 | var valueTask = new ParallelValueTask(42).AsValueTask(); 126 | Assert.That(valueTask.IsCompleted, Is.True); 127 | Assert.That(valueTask.Result, Is.EqualTo(42)); 128 | } 129 | 130 | [TestCase(true)] 131 | [TestCase(false)] 132 | public async Task AsValueTask_GetAsyncSyncResult_ReturnsResultAsynchronously(bool actuallyAsync) 133 | { 134 | var valueTask = ParallelMethod().AsValueTask(); 135 | Assert.That(valueTask.IsCompleted, Is.False); // Always false, even when actuallyAsync is false. 136 | // Tasks are "cold" and are not started until await 137 | Assert.That(await valueTask, Is.EqualTo(42)); 138 | 139 | async ParallelValueTask ParallelMethod() 140 | { 141 | if (actuallyAsync) 142 | { 143 | await Task.Yield(); 144 | } 145 | 146 | return 42; 147 | } 148 | } 149 | 150 | 151 | } -------------------------------------------------------------------------------- /AwaitThreading.Enumerable.Tests/PartitionParallelExtensionsTest.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Collections.Concurrent; 6 | using AwaitThreading.Core.Tasks; 7 | using NUnit.Framework; 8 | using NUnit.Framework.Legacy; 9 | 10 | namespace AwaitThreading.Enumerable.Tests; 11 | 12 | [TestFixture] 13 | [TestOf(typeof(CollectionParallelExtensions))] 14 | public class PartitionParallelExtensionsTest 15 | { 16 | [Test] 17 | public async Task AsParallelAsync_WithOneThread_IteratesOverAllElementsInNaturalOrder() 18 | { 19 | await TestBody(); 20 | return; 21 | 22 | async ParallelTask TestBody() 23 | { 24 | var list = System.Linq.Enumerable.Range(0, 10).ToList(); 25 | var result = new ConcurrentBag(); 26 | 27 | var partitioner = new NotDynamicPartitioner(list); 28 | await foreach (var i in await partitioner.AsParallelAsync(1)) 29 | { 30 | result.Add(i); 31 | } 32 | 33 | CollectionAssert.AreEquivalent(list, result); 34 | } 35 | } 36 | 37 | [TestCase(10, 2)] 38 | [TestCase(10, 3)] 39 | [TestCase(100, 2)] 40 | [TestCase(100, 3)] 41 | [TestCase(100, 4)] 42 | [TestCase(100, 5)] 43 | [TestCase(1, 2)] 44 | [TestCase(0, 2)] 45 | public async Task AsParallelAsync_WithDifferentThreadCount_IteratesOverAllElementsOnce( 46 | int itemsCount, 47 | int threadCount) 48 | { 49 | await TestBody(); 50 | return; 51 | 52 | async ParallelTask TestBody() 53 | { 54 | var list = System.Linq.Enumerable.Range(0, itemsCount).ToList(); 55 | var result = new ConcurrentBag(); 56 | 57 | var partitioner = new NotDynamicPartitioner(list); 58 | await foreach (var i in await partitioner.AsParallelAsync(threadCount)) 59 | { 60 | result.Add(i); 61 | } 62 | 63 | 64 | CollectionAssert.AreEquivalent(list, result); 65 | } 66 | } 67 | 68 | [Test] 69 | public async Task AsAsyncParallel_WithOneThread_IteratesOverAllElementsInNaturalOrder() 70 | { 71 | await TestBody(); 72 | return; 73 | 74 | async ParallelTask TestBody() 75 | { 76 | var list = System.Linq.Enumerable.Range(0, 10).ToList(); 77 | var result = new ConcurrentBag(); 78 | 79 | var partitioner = new NotDynamicPartitioner(list); 80 | await foreach (var i in partitioner.AsAsyncParallel(1)) 81 | { 82 | result.Add(i); 83 | } 84 | 85 | CollectionAssert.AreEquivalent(list, result); 86 | } 87 | } 88 | 89 | [TestCase(10, 2)] 90 | [TestCase(10, 3)] 91 | [TestCase(100, 2)] 92 | [TestCase(100, 3)] 93 | [TestCase(100, 4)] 94 | [TestCase(100, 5)] 95 | [TestCase(1, 2)] 96 | [TestCase(0, 2)] 97 | public async Task AsAsyncParallel_WithDifferentThreadCount_IteratesOverAllElementsOnce( 98 | int itemsCount, 99 | int threadCount) 100 | { 101 | await TestBody(); 102 | return; 103 | 104 | async ParallelTask TestBody() 105 | { 106 | var list = System.Linq.Enumerable.Range(0, itemsCount).ToList(); 107 | var result = new ConcurrentBag(); 108 | 109 | var partitioner = new NotDynamicPartitioner(list); 110 | await foreach (var i in partitioner.AsAsyncParallel(threadCount)) 111 | { 112 | result.Add(i); 113 | } 114 | 115 | 116 | CollectionAssert.AreEquivalent(list, result); 117 | } 118 | } 119 | 120 | private class NotDynamicPartitioner : Partitioner 121 | { 122 | private readonly IReadOnlyList _list; 123 | 124 | public NotDynamicPartitioner(IReadOnlyList list) 125 | { 126 | _list = list; 127 | } 128 | 129 | public override bool SupportsDynamicPartitions => false; 130 | 131 | public override IList> GetPartitions(int partitionCount) 132 | { 133 | if (partitionCount <= 0) 134 | { 135 | throw new ArgumentOutOfRangeException(nameof(partitionCount)); 136 | } 137 | 138 | var partitions = new List>(partitionCount); 139 | var partitionSize = _list.Count / partitionCount; 140 | var remainder = _list.Count % partitionCount; 141 | 142 | var currentIndex = 0; 143 | 144 | for (var i = 0; i < partitionCount; i++) 145 | { 146 | var currentPartitionSize = partitionSize + (i < remainder ? 1 : 0); 147 | partitions.Add(CreatePartitionEnumerator(currentIndex, currentPartitionSize)); 148 | currentIndex += currentPartitionSize; 149 | } 150 | 151 | return partitions; 152 | } 153 | 154 | private IEnumerator CreatePartitionEnumerator(int start, int count) 155 | { 156 | for (var i = 0; i < count; i++) 157 | { 158 | yield return _list[start + i]; 159 | } 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /AwaitThreading.Core.Tests/Tasks/ParallelValueTaskTest.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2025 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using AwaitThreading.Core.Tasks; 6 | #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously 7 | 8 | namespace AwaitThreading.Core.Tests.Tasks; 9 | 10 | [TestFixture] 11 | [TestOf(typeof(ParallelValueTask))] 12 | [TestOf(typeof(ParallelValueTask<>))] 13 | public class ParallelValueTaskTest 14 | { 15 | [Test] 16 | public void SyncVoidResult_IsCompleted_True() 17 | { 18 | var parallelValueTask = ParallelValueTask.CompletedTask; 19 | Assert.That(parallelValueTask.GetAwaiter().IsCompleted, Is.True); 20 | Assert.That(parallelValueTask.GetAwaiter().GetResult, Throws.Nothing); 21 | } 22 | 23 | [Test] 24 | public void SyncResult_GetResult_ReturnsResult() 25 | { 26 | var parallelValueTask = ParallelValueTask.FromResult(42); 27 | Assert.That(parallelValueTask.GetAwaiter().IsCompleted, Is.True); 28 | Assert.That(parallelValueTask.GetAwaiter().GetResult(), Is.EqualTo(42)); 29 | } 30 | 31 | [TestCase(true)] 32 | [TestCase(false)] 33 | public async Task AsyncVoidResult_Await_DoesNotThrow(bool useParallelOperations) 34 | { 35 | await ParallelMethod(); 36 | 37 | async ParallelValueTask ParallelMethod() 38 | { 39 | if (useParallelOperations) 40 | { 41 | await ParallelOperations.Fork(2); 42 | await ParallelOperations.Join(); 43 | } 44 | else 45 | { 46 | await Task.Yield(); 47 | } 48 | } 49 | } 50 | 51 | [TestCase(true)] 52 | [TestCase(false)] 53 | public async Task AsyncException_Await_ThrowsException(bool useParallelOperations) 54 | { 55 | await AssertEx.CheckThrowsAsync(async () => await ParallelMethod()); 56 | 57 | async ParallelValueTask ParallelMethod() 58 | { 59 | if (useParallelOperations) 60 | { 61 | await ParallelOperations.Fork(2); 62 | await ParallelOperations.Join(); 63 | } 64 | else 65 | { 66 | await Task.Yield(); 67 | } 68 | 69 | throw new ArgumentOutOfRangeException(); 70 | } 71 | } 72 | 73 | [TestCase(true)] 74 | [TestCase(false)] 75 | public async Task AsyncResult_Await_ReturnsResult(bool useParallelOperations) 76 | { 77 | var result = await ParallelMethod(); 78 | Assert.That(result, Is.EqualTo(42)); 79 | 80 | async ParallelValueTask ParallelMethod() 81 | { 82 | if (useParallelOperations) 83 | { 84 | await ParallelOperations.Fork(2); 85 | await ParallelOperations.Join(); 86 | } 87 | else 88 | { 89 | await Task.Yield(); 90 | } 91 | 92 | return 42; 93 | } 94 | } 95 | 96 | [TestCase(true)] 97 | [TestCase(false)] 98 | public async Task AsyncWithResultException_Await_ThrowsException(bool useParallelOperations) 99 | { 100 | await AssertEx.CheckThrowsAsync(async () => await ParallelMethod()); 101 | 102 | async ParallelValueTask ParallelMethod() 103 | { 104 | if (useParallelOperations) 105 | { 106 | await ParallelOperations.Fork(2); 107 | await ParallelOperations.Join(); 108 | } 109 | else 110 | { 111 | await Task.Yield(); 112 | } 113 | 114 | throw new ArgumentOutOfRangeException(); 115 | } 116 | } 117 | 118 | [Test] 119 | public void AsyncVoidResultWithoutAwaits_IsCompleted_False() 120 | { 121 | var parallelValueTask = ParallelMethod(); 122 | Assert.That(parallelValueTask.GetAwaiter().IsCompleted, Is.False); 123 | Assert.That(parallelValueTask.GetAwaiter().GetResult, Throws.TypeOf()); 124 | 125 | async ParallelValueTask ParallelMethod() 126 | { 127 | } 128 | } 129 | 130 | [Test] 131 | public void AsyncResultWithoutAwaits_IsCompleted_False() 132 | { 133 | var parallelValueTask = ParallelMethod(); 134 | Assert.That(parallelValueTask.GetAwaiter().IsCompleted, Is.False); 135 | Assert.That(parallelValueTask.GetAwaiter().GetResult, Throws.TypeOf()); 136 | 137 | async ParallelValueTask ParallelMethod() 138 | { 139 | return 42; 140 | } 141 | } 142 | 143 | [Test] 144 | public async Task AsyncVoidResultWithoutAwaits_Await_DoesNotThrow() 145 | { 146 | await ParallelMethod(); 147 | 148 | async ParallelValueTask ParallelMethod() 149 | { 150 | } 151 | } 152 | 153 | [Test] 154 | public async Task AsyncResultWithoutAwaits_Await_ReturnsResult() 155 | { 156 | var result = await ParallelMethod(); 157 | Assert.That(result, Is.EqualTo(42)); 158 | 159 | async ParallelValueTask ParallelMethod() 160 | { 161 | return 42; 162 | } 163 | } 164 | 165 | [Test] 166 | public async Task MethodsComposition_AwaitedFromParallelTask_ParallelOperationsWork() 167 | { 168 | await TestBody(); 169 | 170 | async ParallelValueTask TestBody() 171 | { 172 | var parallelCounter = new ParallelCounter(); 173 | await ForkingMethod(); 174 | parallelCounter.Increment(); 175 | await JoiningMethod(); 176 | 177 | Assert.That(parallelCounter.Count, Is.EqualTo(2)); 178 | } 179 | 180 | async ParallelValueTask ForkingMethod() 181 | { 182 | await ParallelOperations.Fork(2); 183 | return 42; 184 | } 185 | 186 | async ParallelValueTask JoiningMethod() 187 | { 188 | await ParallelOperations.Join(); 189 | } 190 | } 191 | } -------------------------------------------------------------------------------- /AwaitThreading.Core.Tests/CoreOperationsTests.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using AwaitThreading.Core.Context; 6 | using AwaitThreading.Core.Diagnostics; 7 | using AwaitThreading.Core.Operations; 8 | using AwaitThreading.Core.Tasks; 9 | 10 | namespace AwaitThreading.Core.Tests; 11 | 12 | [TestFixture] 13 | [TestOf(typeof(ForkingTask))] 14 | [TestOf(typeof(JoiningTask))] 15 | public class CoreOperationsTests : BaseClassWithParallelContextValidation 16 | { 17 | [Test] 18 | [TestCase(1)] 19 | [TestCase(2)] 20 | [TestCase(10)] 21 | public async Task Fork_NThreadsStarted_NThreadsExecuted(int n) 22 | { 23 | var counter = new ParallelCounter(); 24 | await TestBody().AsTask(); 25 | counter.AssertCount(n); 26 | return; 27 | 28 | async ParallelTask TestBody() 29 | { 30 | await new ForkingTask(n); 31 | counter.Increment(); 32 | await new JoiningTask(); 33 | } 34 | } 35 | 36 | [Test] 37 | [TestCase(1)] 38 | [TestCase(2)] 39 | [TestCase(3)] 40 | public async Task NestedOperation_Fork_ForksAreMultiplied(int n) 41 | { 42 | var counter = new ParallelCounter(); 43 | await TestBody().AsTask(); 44 | counter.AssertCount(n * n); 45 | return; 46 | 47 | async ParallelTask TestBody() 48 | { 49 | await new ForkingTask(n); 50 | await NestedFork(); 51 | counter.Increment(); 52 | Logger.Log("I'm incrementing"); 53 | await new JoiningTask(); 54 | await new JoiningTask(); 55 | } 56 | 57 | async ParallelTask NestedFork() 58 | { 59 | await new ForkingTask(n); 60 | } 61 | } 62 | 63 | [Test] 64 | public async Task NestedOperation_Join_JoinAffectsExternalMethod() 65 | { 66 | var counter = new ParallelCounter(); 67 | await TestBody().AsTask(); 68 | counter.AssertCount(1); 69 | return; 70 | 71 | async ParallelTask TestBody() 72 | { 73 | await new ForkingTask(2); 74 | await NestedJoin(); 75 | counter.Increment(); 76 | } 77 | 78 | async ParallelTask NestedJoin() 79 | { 80 | await new JoiningTask(); 81 | } 82 | } 83 | 84 | [Test] 85 | public async Task NestedOperation_MultipleNested_AffectsExternalMethod() 86 | { 87 | var counterAfterForks = new ParallelCounter(); 88 | var counterAfterJoins = new ParallelCounter(); 89 | await TestBody().AsTask(); 90 | counterAfterForks.AssertCount(4); 91 | counterAfterJoins.AssertCount(1); 92 | return; 93 | 94 | async ParallelTask TestBody() 95 | { 96 | await NestedFork1(); 97 | counterAfterForks.Increment(); 98 | await NestedJoin1(); 99 | counterAfterJoins.Increment(); 100 | } 101 | 102 | async ParallelTask NestedFork1() 103 | { 104 | await new ForkingTask(2); 105 | await NestedFork2(); 106 | } 107 | 108 | async ParallelTask NestedFork2() 109 | { 110 | await new ForkingTask(2); 111 | } 112 | 113 | async ParallelTask NestedJoin1() 114 | { 115 | await new JoiningTask(); 116 | await NestedJoin2(); 117 | } 118 | 119 | async ParallelTask NestedJoin2() 120 | { 121 | await new JoiningTask(); 122 | } 123 | } 124 | 125 | [Test] 126 | public async Task SharedState_ReferencesCreatedBeforeFork_SameReferenceAfterFork() 127 | { 128 | var res = await TestBody().AsTask(); 129 | Assert.That(res[0], Is.EqualTo(1)); 130 | Assert.That(res[1], Is.EqualTo(1)); 131 | return; 132 | 133 | async ParallelTask TestBody() 134 | { 135 | var sharedArray = new int[2]; 136 | await new ForkingTask(2); 137 | sharedArray[ParallelContextStorage.GetTopFrameId()] = 1; 138 | await new JoiningTask(); 139 | return sharedArray; 140 | } 141 | } 142 | 143 | [Test] 144 | public async Task SharedState_ReferencesCreatedAfterFork_ReferencesAreDifferent() 145 | { 146 | var res = await TestBody().AsTask(); 147 | Assert.That(res[0], Is.Not.SameAs(res[1])); 148 | return; 149 | 150 | async ParallelTask TestBody() 151 | { 152 | var sharedArray = new object[2]; 153 | await new ForkingTask(2); 154 | var localObject = new object(); 155 | sharedArray[ParallelContextStorage.GetTopFrameId()] = localObject; 156 | await new JoiningTask(); 157 | return sharedArray; 158 | } 159 | } 160 | 161 | [Test] 162 | public async Task SharedStateWithTargetedJoin_ReferencesCreatedAfterFork_ReferenceFromThread0IsAvailableAfterJoin() 163 | { 164 | var res = await TestBody().AsTask(); 165 | Assert.That(res.SharedArray[0], Is.SameAs(res.ValueAfterJoin)); 166 | return; 167 | 168 | async ParallelTask<(object[] SharedArray, object ValueAfterJoin)> TestBody() 169 | { 170 | var sharedArray = new object[2]; 171 | await new ForkingTask(2); 172 | var localObject = new object(); 173 | sharedArray[ParallelContextStorage.GetTopFrameId()] = localObject; 174 | await new TargetedJoiningTask(); 175 | return (sharedArray, localObject); 176 | } 177 | } 178 | 179 | [Test] 180 | public async Task SharedState_ValueTypeIsDefinedBeforeFork_ChangedSeparately() 181 | { 182 | var res = await TestBody().AsTask(); 183 | Assert.That(res.SharedArray[0], Is.EqualTo(2)); 184 | Assert.That(res.SharedArray[1], Is.EqualTo(2)); 185 | Assert.That(res.ValueAfterJoin, Is.EqualTo(2)); 186 | return; 187 | 188 | async ParallelTask<(int[] SharedArray, object ValueAfterJoin)> TestBody() 189 | { 190 | var sharedArray = new int[2]; 191 | var sharedInt = 1; 192 | 193 | await new ForkingTask(2); 194 | var incrementedValue = Interlocked.Increment(ref sharedInt); 195 | sharedArray[ParallelContextStorage.GetTopFrameId()] = incrementedValue; 196 | await new JoiningTask(); 197 | 198 | return (sharedArray, sharedInt); 199 | } 200 | } 201 | } -------------------------------------------------------------------------------- /AwaitThreading.Core/Tasks/ParallelTaskImpl.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2023 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Diagnostics; 6 | using System.Runtime.CompilerServices; 7 | using AwaitThreading.Core.Context; 8 | using AwaitThreading.Core.Diagnostics; 9 | 10 | namespace AwaitThreading.Core.Tasks; 11 | 12 | internal sealed class ParallelTaskImpl 13 | { 14 | [ThreadStatic] // TODO: clear? 15 | private static ParallelTaskResult? _parallelResult; 16 | 17 | // note: volatile is not required 18 | private IContinuationInvoker? _continuation; 19 | 20 | private Action? _onDemandStartAction; 21 | 22 | public void SetResult(ParallelTaskResult result) 23 | { 24 | _parallelResult = result; 25 | if (_continuation is not { } continuation) 26 | { 27 | throw new InvalidOperationException("Continuation should be set before result in parallel behaviour"); 28 | } 29 | 30 | continuation.Invoke(); 31 | } 32 | 33 | internal ParallelTaskResult GetResult() 34 | { 35 | if (_continuation is null || !_parallelResult.HasValue) 36 | { 37 | Assertion.ThrowInvalidDirectGetResultCall(); 38 | } 39 | 40 | return _parallelResult.Value; 41 | } 42 | 43 | public void ParallelOnCompleted(TStateMachine stateMachine) 44 | where TStateMachine : IAsyncStateMachine 45 | { 46 | var onDemandStartAction = Interlocked.Exchange(ref _onDemandStartAction, null); 47 | if (onDemandStartAction is null) 48 | { 49 | Assertion.ThrowInvalidSecondAwaitOfParallelTask(); 50 | } 51 | 52 | // continuation should run with the same context as onDemandStartAction finishes with (it can contain fork or join) 53 | _continuation = new ParallelContinuationInvoker(stateMachine); 54 | 55 | // clear the parallel context here since the thread will go to the thread pool after this method 56 | var parallelContext = ParallelContextStorage.CaptureAndClear(); 57 | 58 | // onDemandStartAction should have the same parallelContext as we have at the moment of awaiting, 59 | // and since we run it via Task.Run, we should pass the context 60 | Task.Run( 61 | () => 62 | { 63 | ParallelContextStorage.Restore(parallelContext); 64 | try 65 | { 66 | onDemandStartAction.Invoke(); 67 | } 68 | finally 69 | { 70 | ParallelContextStorage.ClearButNotExpected(); 71 | } 72 | }); 73 | } 74 | 75 | public void OnCompleted(Action continuation) 76 | { 77 | OnCompletedInternal(continuation); 78 | } 79 | 80 | public void UnsafeOnCompleted(Action continuation) 81 | { 82 | // TODO: we need a proper implementation: one restores the execution context and another one doesn't 83 | OnCompleted(continuation); 84 | } 85 | 86 | private void OnCompletedInternal(Action continuation) 87 | { 88 | var onDemandStartAction = Interlocked.Exchange(ref _onDemandStartAction, null); 89 | if (onDemandStartAction is null) 90 | { 91 | Assertion.ThrowInvalidSecondAwaitOfParallelTask(); 92 | } 93 | 94 | var continuationInvoker = new RegularContinuationInvokerWithFrameProtection(continuation); 95 | 96 | _continuation = continuationInvoker; 97 | Task.Run( 98 | () => 99 | { 100 | try 101 | { 102 | onDemandStartAction.Invoke(); 103 | } 104 | finally 105 | { 106 | ParallelContextStorage.ClearButNotExpected(); 107 | } 108 | }); 109 | } 110 | 111 | private sealed class RegularContinuationInvokerWithFrameProtection : IContinuationInvoker 112 | { 113 | private Action? _action; 114 | public RegularContinuationInvokerWithFrameProtection(Action action) 115 | { 116 | _action = action; 117 | } 118 | 119 | public void Invoke() 120 | { 121 | // Note: in general, original context (at the moment we are awaiting the `Task` method) should be empty, 122 | // except when we are in the sync part of async Task method. But even if it's not, we are clearing it before 123 | // yielding to async continuation of regular Task, since we can't afford ParallelContext to be passed to the 124 | // regular Task method and therefore allow `await ParallelTask (fork) -> await Task -> await ParallelTask (join)`. 125 | // Otherwise, after exiting the `ParallelTask (join)`, we'll pass the control flow to the standard Task 126 | // method builder, and it can schedule the continuation on other thread and return the thread to the Thread 127 | // pool with ParallelContext set. 128 | // So, we always guarantee that nested ParallelTask method is executed with empty ParallelContext 129 | // at the beginning. When the nested ParallelTask method nevertheless finishes with some ParallelContext 130 | // (e.g. it contains fork), context will be cleared and only one thread will proceed, 131 | // raising `BadAwaitExceptionDispatchInfo` to the awaiter. 132 | var parallelContext = ParallelContextStorage.CaptureAndClear(); 133 | 134 | var action = Interlocked.Exchange(ref _action, null); 135 | if (action is null) 136 | { 137 | // Already invoked action once. Continuation in standard async Task method builder can't be 138 | // called twice, so we need to just return (unless we want to break the thread pool thread with 139 | // an exception). But it can only happen when we've got a ParallelContext and, therefore, already 140 | // notified the continuation with 'BadAwaitExceptionDispatchInfo' in other thread. 141 | Debug.Assert(!parallelContext.IsEmpty); 142 | return; 143 | } 144 | 145 | if (!parallelContext.IsEmpty) 146 | { 147 | // Note: it works, but we rely on the fact that the same thread will run the continuation. 148 | // It's required for the forking workload, but it can be changed in the future for cases 149 | // when normal task awaits ParallelTask, so the continuation can be re-scheduled. 150 | _parallelResult = new ParallelTaskResult(Assertion.BadAwaitExceptionDispatchInfo); 151 | } 152 | 153 | action.Invoke(); 154 | } 155 | } 156 | 157 | /// 158 | /// Instead of running the state machine right now, we are preserving it to run later on-demand. 159 | /// With this approach, we can control parallel context set\clear depending on the way our task is awaited 160 | /// (e.g. using regular UnsafeOnCompleted or ParallelOnCompleted) 161 | /// 162 | public void SetStateMachine(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine 163 | { 164 | var stateMachineLocal = stateMachine; 165 | _onDemandStartAction = () => stateMachineLocal.MoveNext(); //TODO: optimize allocations? 166 | } 167 | } -------------------------------------------------------------------------------- /AwaitThreading.Enumerable.Tests/CollectionParallelExtensionsTest.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Collections.Concurrent; 6 | using System.Diagnostics.CodeAnalysis; 7 | using AwaitThreading.Core.Tasks; 8 | using NUnit.Framework; 9 | using NUnit.Framework.Legacy; 10 | 11 | namespace AwaitThreading.Enumerable.Tests; 12 | 13 | [TestFixture] 14 | [TestOf(typeof(CollectionParallelExtensions))] 15 | public class CollectionParallelExtensionsTest 16 | { 17 | [Test] 18 | public async Task AsParallelAsync_WithOneThread_IteratesOverAllElementsInNaturalOrder() 19 | { 20 | await TestBody(); 21 | return; 22 | 23 | async ParallelTask TestBody() 24 | { 25 | var list = System.Linq.Enumerable.Range(0, 10).ToList(); 26 | var result = new List(); 27 | await foreach (var i in await list.AsParallelAsync(1)) 28 | { 29 | result.Add(i); 30 | } 31 | 32 | CollectionAssert.AreEquivalent(list, result); 33 | } 34 | } 35 | 36 | [TestCase(10, 2)] 37 | [TestCase(10, 3)] 38 | [TestCase(100, 2)] 39 | [TestCase(100, 3)] 40 | [TestCase(100, 4)] 41 | [TestCase(100, 5)] 42 | [TestCase(1, 2)] 43 | [TestCase(0, 2)] 44 | public async Task AsParallelAsync_WithDifferentThreadCount_IteratesOverAllElementsOnce( 45 | int itemsCount, 46 | int threadCount) 47 | { 48 | await TestBody(); 49 | return; 50 | 51 | async ParallelTask TestBody() 52 | { 53 | var list = System.Linq.Enumerable.Range(0, itemsCount).ToList(); 54 | var result = new ConcurrentBag(); 55 | await foreach (var i in await list.AsParallelAsync(threadCount)) 56 | { 57 | result.Add(i); 58 | } 59 | 60 | CollectionAssert.AreEquivalent(list, result); 61 | } 62 | } 63 | 64 | [Test] 65 | public async Task AsParallelLazyAsync_WithOneThread_IteratesOverAllElementsInNaturalOrder() 66 | { 67 | await TestBody(); 68 | return; 69 | 70 | async ParallelTask TestBody() 71 | { 72 | var list = System.Linq.Enumerable.Range(0, 10).ToList(); 73 | var result = new List(); 74 | await foreach (var i in list.AsAsyncParallel(1)) 75 | { 76 | result.Add(i); 77 | } 78 | 79 | CollectionAssert.AreEquivalent(list, result); 80 | } 81 | } 82 | 83 | [TestCase(10, 1)] 84 | [TestCase(10, 2)] 85 | [TestCase(10, 3)] 86 | [TestCase(100, 2)] 87 | [TestCase(100, 3)] 88 | [TestCase(100, 4)] 89 | [TestCase(100, 5)] 90 | [TestCase(1, 2)] 91 | [TestCase(0, 2)] 92 | public async Task AsParallelLazyAsync_WithDifferentThreadCount_IteratesOverAllElementsOnce( 93 | int itemsCount, 94 | int threadCount) 95 | { 96 | await TestBody(); 97 | return; 98 | 99 | async ParallelTask TestBody() 100 | { 101 | var list = System.Linq.Enumerable.Range(0, itemsCount).ToList(); 102 | var result = new ConcurrentBag(); 103 | await foreach (var i in list.AsAsyncParallel(threadCount)) 104 | { 105 | result.Add(i); 106 | } 107 | 108 | CollectionAssert.AreEquivalent(list, result); 109 | } 110 | } 111 | 112 | [Test] 113 | [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] 114 | public async Task AsParallelAsyncEnumerable_WithOneThread_IteratesOverAllElementsInNaturalOrder() 115 | { 116 | await TestBody(); 117 | return; 118 | 119 | async ParallelTask TestBody() 120 | { 121 | var list = System.Linq.Enumerable.Range(0, 10); 122 | var result = new List(); 123 | await foreach (var i in await list.AsParallelAsync(1)) 124 | { 125 | result.Add(i); 126 | } 127 | 128 | CollectionAssert.AreEquivalent(list, result); 129 | } 130 | } 131 | 132 | [TestCase(10, 1)] 133 | [TestCase(10, 2)] 134 | [TestCase(10, 3)] 135 | [TestCase(100, 2)] 136 | [TestCase(100, 3)] 137 | [TestCase(100, 4)] 138 | [TestCase(100, 5)] 139 | [TestCase(1, 2)] 140 | [TestCase(0, 2)] 141 | [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] 142 | public async Task AsParallelAsyncEnumerable_WithDifferentThreadCount_IteratesOverAllElementsOnce( 143 | int itemsCount, 144 | int threadCount) 145 | { 146 | await TestBody(); 147 | return; 148 | 149 | async ParallelTask TestBody() 150 | { 151 | var list = System.Linq.Enumerable.Range(0, itemsCount); 152 | var result = new ConcurrentBag(); 153 | await foreach (var i in await list.AsParallelAsync(threadCount)) 154 | { 155 | result.Add(i); 156 | } 157 | 158 | CollectionAssert.AreEquivalent(list, result); 159 | } 160 | } 161 | 162 | [Test] 163 | [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] 164 | public async Task AsAsyncParallelEnumerable_WithOneThread_IteratesOverAllElementsInNaturalOrder() 165 | { 166 | await TestBody(); 167 | return; 168 | 169 | async ParallelTask TestBody() 170 | { 171 | var list = System.Linq.Enumerable.Range(0, 10); 172 | var result = new List(); 173 | await foreach (var i in list.AsAsyncParallel(1)) 174 | { 175 | result.Add(i); 176 | } 177 | 178 | CollectionAssert.AreEquivalent(list, result); 179 | } 180 | } 181 | 182 | [TestCase(10, 1)] 183 | [TestCase(10, 2)] 184 | [TestCase(10, 3)] 185 | [TestCase(100, 2)] 186 | [TestCase(100, 3)] 187 | [TestCase(100, 4)] 188 | [TestCase(100, 5)] 189 | [TestCase(1, 2)] 190 | [TestCase(0, 2)] 191 | [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] 192 | public async Task AsAsyncParallelEnumerable_WithDifferentThreadCount_IteratesOverAllElementsOnce( 193 | int itemsCount, 194 | int threadCount) 195 | { 196 | await TestBody(); 197 | return; 198 | 199 | async ParallelTask TestBody() 200 | { 201 | var list = System.Linq.Enumerable.Range(0, itemsCount); 202 | var result = new ConcurrentBag(); 203 | await foreach (var i in list.AsAsyncParallel(threadCount)) 204 | { 205 | result.Add(i); 206 | } 207 | 208 | CollectionAssert.AreEquivalent(list, result); 209 | } 210 | } 211 | 212 | [Test] 213 | public async Task AsParallelAsync_WithZeroThreads_ThrowsException() 214 | { 215 | await TestBody(); 216 | return; 217 | 218 | async ParallelTask TestBody() 219 | { 220 | var gotException = false; 221 | try 222 | { 223 | var list = System.Linq.Enumerable.Range(0, 10).ToList(); 224 | await foreach (var unused in await list.AsParallelAsync(0)) 225 | { 226 | } 227 | } 228 | catch (ArgumentOutOfRangeException) 229 | { 230 | gotException = true; 231 | } 232 | 233 | Assert.That(gotException, Is.True); 234 | } 235 | } 236 | } -------------------------------------------------------------------------------- /AwaitThreading.Core.Tests/ParallelTaskMethodBuilderTests.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using AwaitThreading.Core.Context; 6 | using AwaitThreading.Core.Operations; 7 | using AwaitThreading.Core.Tasks; 8 | 9 | #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously 10 | namespace AwaitThreading.Core.Tests; 11 | 12 | [TestFixture] 13 | [TestOf(typeof(ParallelTaskMethodBuilder))] 14 | [TestOf(typeof(ParallelTaskMethodBuilder<>))] 15 | public class ParallelTaskMethodBuilderTests : BaseClassWithParallelContextValidation 16 | { 17 | [Test] 18 | public async Task Await_VoidResultSetSync_ResultReturned() 19 | { 20 | await TestBody(); 21 | return; 22 | 23 | async ParallelTask TestBody() 24 | { 25 | await GetResult(); 26 | } 27 | 28 | async ParallelTask GetResult() 29 | { 30 | } 31 | } 32 | 33 | [Test] 34 | public async Task Await_IntResultSetSync_ResultReturned() 35 | { 36 | var result = await TestBody(); 37 | Assert.That(result, Is.EqualTo(42)); 38 | return; 39 | 40 | async ParallelTask TestBody() 41 | { 42 | return await GetResult(); 43 | } 44 | 45 | async ParallelTask GetResult() 46 | { 47 | return 42; 48 | } 49 | } 50 | 51 | [Test] 52 | public async Task Await_ResultSetAfterNonParallelTaskAwait_ResultReturned() 53 | { 54 | var result = await TestBody(); 55 | Assert.That(result, Is.EqualTo(42)); 56 | return; 57 | 58 | async ParallelTask TestBody() 59 | { 60 | return await GetResult(); 61 | } 62 | 63 | async ParallelTask GetResult() 64 | { 65 | await Task.Yield(); 66 | return 42; 67 | } 68 | } 69 | 70 | [Test] 71 | public async Task Await_ResultSetAfterParallelOperations_ResultReturned() 72 | { 73 | var result = await TestBody(); 74 | Assert.That(result, Is.EqualTo(42)); 75 | return; 76 | 77 | async ParallelTask TestBody() 78 | { 79 | return await GetResult(); 80 | } 81 | 82 | async ParallelTask GetResult() 83 | { 84 | await new ForkingTask(2); 85 | await new JoiningTask(); 86 | return 42; 87 | } 88 | } 89 | 90 | [Test] 91 | public async Task Await_ResultSetInTwoThreads_BothReturned() 92 | { 93 | var result = await TestBody(); 94 | Assert.That(result, Is.EqualTo(3)); 95 | return; 96 | 97 | async ParallelTask TestBody() 98 | { 99 | var sum = new ParallelCounter(); 100 | var res = await ForkAndGetResult(); 101 | sum.Add(res); 102 | await new JoiningTask(); 103 | return sum.Count; 104 | } 105 | 106 | async ParallelTask ForkAndGetResult() 107 | { 108 | await new ForkingTask(2); 109 | return ParallelContextStorage.GetTopFrameId() == 0 ? 1 : 2; 110 | } 111 | } 112 | 113 | [Test] 114 | public async Task AwaitVoid_ExceptionIsThrownInSyncContext_ExceptionIsPropagated() 115 | { 116 | await AssertEx.CheckThrowsAsync(() => TestBody().AsTask()); 117 | return; 118 | 119 | async ParallelTask TestBody() 120 | { 121 | throw new ArgumentOutOfRangeException(); 122 | } 123 | } 124 | 125 | [Test] 126 | public async Task AwaitWithResult_ExceptionIsThrownInSyncContext_ExceptionIsPropagated() 127 | { 128 | await AssertEx.CheckThrowsAsync(() => TestBody().AsTask()); 129 | return; 130 | 131 | async ParallelTask TestBody() 132 | { 133 | throw new ArgumentOutOfRangeException(); 134 | } 135 | } 136 | 137 | [Test] 138 | public async Task AwaitVoid_ExceptionIsThrownInSubMethodSync_ExceptionIsPropagated() 139 | { 140 | await AssertEx.CheckThrowsAsync(() => TestBody().AsTask()); 141 | return; 142 | 143 | async ParallelTask TestBody() 144 | { 145 | await InnerMethod(); 146 | } 147 | 148 | async ParallelTask InnerMethod() 149 | { 150 | throw new ArgumentOutOfRangeException(); 151 | } 152 | } 153 | 154 | [Test] 155 | public async Task AwaitWithResult_ExceptionIsThrownInSubMethodSync_ExceptionIsPropagated() 156 | { 157 | await AssertEx.CheckThrowsAsync(() => TestBody().AsTask()); 158 | return; 159 | 160 | async ParallelTask TestBody() 161 | { 162 | return await InnerMethod(); 163 | } 164 | 165 | async ParallelTask InnerMethod() 166 | { 167 | throw new ArgumentOutOfRangeException(); 168 | } 169 | } 170 | 171 | [Test] 172 | public async Task Await_ExceptionIsThrownInAsyncContextDepth_ExceptionIsPropagated() 173 | { 174 | await AssertEx.CheckThrowsAsync(() => TestBody().AsTask()); 175 | return; 176 | 177 | async ParallelTask TestBody() 178 | { 179 | await new ForkingTask(2); 180 | await new JoiningTask(); 181 | throw new ArgumentOutOfRangeException(); 182 | } 183 | } 184 | 185 | [Test] 186 | public async Task Await_MultipleCallsOfParallelMethod_StackTrackDoNotGrow() 187 | { 188 | await TestBody(); 189 | return; 190 | 191 | async ParallelTask TestBody() 192 | { 193 | await DoSomething(); 194 | var stackTrace1 = Environment.StackTrace; 195 | await DoSomething(); 196 | var stackTrace2 = Environment.StackTrace; 197 | 198 | Assert.That(stackTrace1, Is.EqualTo(stackTrace2).UsingStringLinesCountEquality()); 199 | } 200 | 201 | async ParallelTask DoSomething() 202 | { 203 | await new ForkingTask(2); 204 | await new JoiningTask(); 205 | } 206 | } 207 | 208 | [Test] 209 | public async Task Await_MultipleForks_StackTrackDoNotGrow() 210 | { 211 | await TestBody(); 212 | return; 213 | 214 | async ParallelTask TestBody() 215 | { 216 | await new ForkingTask(2); 217 | var stackTrace1 = Environment.StackTrace; 218 | await new ForkingTask(2); 219 | var stackTrace2 = Environment.StackTrace; 220 | await new JoiningTask(); 221 | await new JoiningTask(); 222 | 223 | Assert.That(stackTrace1, Is.EqualTo(stackTrace2).UsingStringLinesCountEquality()); 224 | } 225 | } 226 | 227 | [Test] 228 | public async Task Await_MultipleCompleted_StackTrackDoNotGrow() 229 | { 230 | await TestBody(); 231 | return; 232 | 233 | async ParallelTask TestBody() 234 | { 235 | await GetResult(); 236 | var stackTrace1 = Environment.StackTrace; 237 | await GetResult(); 238 | var stackTrace2 = Environment.StackTrace; 239 | 240 | Assert.That(stackTrace1, Is.EqualTo(stackTrace2).UsingStringLinesCountEquality()); 241 | } 242 | 243 | async ParallelTask GetResult() 244 | { 245 | return 42; 246 | } 247 | } 248 | 249 | [Test, /*Ignore("This test can also fail with standard `Task`")*/] 250 | public async Task Await_MultipleNotCompleted_StackTrackDoNotGrow() 251 | { 252 | await TestBody(); 253 | return; 254 | 255 | async ParallelTask TestBody() 256 | { 257 | await GetResult(); 258 | var stackTrace1 = Environment.StackTrace; 259 | await GetResult(); 260 | var stackTrace2 = Environment.StackTrace; 261 | 262 | Assert.That(stackTrace1.Split('\n').Length, Is.EqualTo(stackTrace2.Split('\n').Length)); 263 | } 264 | 265 | async ParallelTask GetResult() 266 | { 267 | await Task.Yield(); 268 | return 42; 269 | } 270 | } 271 | 272 | [Test] 273 | public async Task Await_MultipleCallsOfNestedParallelMethod_StackTrackDoNotGrow() 274 | { 275 | await TestBody(); 276 | return; 277 | 278 | async ParallelTask TestBody() 279 | { 280 | await DoSomething(); 281 | var stackTrace1 = Environment.StackTrace; 282 | await DoSomething(); 283 | var stackTrace2 = Environment.StackTrace; 284 | 285 | Assert.That(stackTrace1, Is.EqualTo(stackTrace2).UsingStringLinesCountEquality()); 286 | } 287 | 288 | async ParallelTask DoSomething() 289 | { 290 | await DoSomethingNested(); 291 | } 292 | 293 | async ParallelTask DoSomethingNested() 294 | { 295 | await new ForkingTask(2); 296 | await new JoiningTask(); 297 | } 298 | } 299 | } -------------------------------------------------------------------------------- /Benchmarks/ParallelForeachBenchmark.cs: -------------------------------------------------------------------------------- 1 | // // MIT License 2 | // // Copyright (c) 2024 Saltuk Konstantin 3 | // // See the LICENSE file in the project root for more information. 4 | // 5 | // using AwaitThreading.Core; 6 | // using AwaitThreading.Enumerable; 7 | // using BenchmarkDotNet.Attributes; 8 | // using BenchmarkDotNet.Configs; 9 | // 10 | // namespace Benchmarks; 11 | // 12 | // [MemoryDiagnoser] 13 | // [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByMethod)] 14 | // // [DisassemblyDiagnoser(printSource: true)] 15 | // public class ParallelForeachBenchmark 16 | // { 17 | // private List _data = null!; 18 | // 19 | // [Params( 20 | // 10 21 | // // , 100 22 | // // , 1000 23 | // //, 10000 24 | // // , 20000 25 | // // , 40000 26 | // // , 80000 27 | // , 160000 28 | // )] 29 | // public int ListLength; 30 | // 31 | // [Params( 32 | // 1, 33 | // // 2, 4, 34 | // 8 35 | // )] 36 | // public int ThreadsCount; 37 | // 38 | // [GlobalSetup] 39 | // public void Setup() 40 | // { 41 | // _data = Enumerable.Range(0, ListLength).ToList(); 42 | // } 43 | // 44 | // class Calculator 45 | // { 46 | // public bool Bool; 47 | // 48 | // public void Calculate(double value) 49 | // { 50 | // for (int i = 0; i < 100; i++) 51 | // { 52 | // var calculated = (((value / 3 + 1.1) * 4.5 + value + i) * 1.3 + 2 * value) / 1.1; 53 | // if (Math.Abs(calculated) < 0.00001) 54 | // { 55 | // Bool = !Bool; 56 | // } 57 | // } 58 | // } 59 | // } 60 | // 61 | // // [Benchmark] 62 | // // public bool ParallelForEach() 63 | // // { 64 | // // var calculator = new Calculator(); 65 | // // Parallel.ForEach( 66 | // // _data, 67 | // // new ParallelOptions { MaxDegreeOfParallelism = ThreadsCount }, 68 | // // i => 69 | // // { 70 | // // calculator.Calculate(i); 71 | // // }); 72 | // // return calculator.Bool; 73 | // // } 74 | // // 75 | // // [Benchmark] 76 | // // public bool AsParallel() 77 | // // { 78 | // // var calculator = new Calculator(); 79 | // // _data.AsParallel() 80 | // // .WithDegreeOfParallelism(ThreadsCount) 81 | // // .ForAll( 82 | // // i => 83 | // // { 84 | // // calculator.Calculate(i); 85 | // // }); 86 | // // return calculator.Bool; 87 | // // } 88 | // // 89 | // // [Benchmark] 90 | // // public bool AsParallelAsync() 91 | // // { 92 | // // return DoAsync().AsTask().Result; 93 | // // 94 | // // async ParallelTask DoAsync() 95 | // // { 96 | // // var calculator = new Calculator(); 97 | // // await foreach (var i in await _data.AsParallelAsync(ThreadsCount)) 98 | // // { 99 | // // calculator.Calculate(i); 100 | // // } 101 | // // 102 | // // return calculator.Bool; 103 | // // } 104 | // // } 105 | // 106 | // [Benchmark] 107 | // public bool AsParallelLazyAsync() 108 | // { 109 | // return DoAsync().AsTask().Result; 110 | // 111 | // async ParallelTask DoAsync() 112 | // { 113 | // var calculator = new Calculator(); 114 | // await foreach (var i in _data.AsAsyncParallel(ThreadsCount)) 115 | // { 116 | // calculator.Calculate(i); 117 | // } 118 | // 119 | // return calculator.Bool; 120 | // } 121 | // } 122 | // } 123 | // 124 | // /* 125 | // BenchmarkDotNet v0.13.12, macOS Sonoma 14.4.1 (23E224) [Darwin 23.4.0] 126 | // Apple M1, 1 CPU, 8 logical and 8 physical cores 127 | // .NET SDK 6.0.100 128 | // [Host] : .NET 6.0.0 (6.0.21.52210), Arm64 RyuJIT AdvSIMD 129 | // DefaultJob : .NET 6.0.0 (6.0.21.52210), Arm64 RyuJIT AdvSIMD 130 | // 131 | // 132 | // | Method | ListLength | ThreadsCount | Mean | Error | StdDev | Gen0 | Allocated | 133 | // |-------------------- |----------- |------------- |--------------:|-----------:|-----------:|-------:|----------:| 134 | // | ParallelForEach | 10 | 1 | 1.428 us | 0.0267 us | 0.0223 us | 0.8907 | 1.82 KB | 135 | // | ParallelForEach | 10 | 2 | 2.474 us | 0.0474 us | 0.0751 us | 0.9880 | 1.97 KB | 136 | // | ParallelForEach | 10 | 4 | 3.985 us | 0.0443 us | 0.0414 us | 1.1292 | 2.27 KB | 137 | // | ParallelForEach | 10 | 8 | 4.441 us | 0.0571 us | 0.0534 us | 1.2817 | 2.58 KB | 138 | // | ParallelForEach | 10000 | 1 | 874.534 us | 0.2852 us | 0.2667 us | - | 1.83 KB | 139 | // | ParallelForEach | 10000 | 2 | 448.758 us | 0.2845 us | 0.2662 us | 0.4883 | 1.97 KB | 140 | // | ParallelForEach | 10000 | 4 | 268.018 us | 2.0178 us | 1.7887 us | 0.9766 | 2.38 KB | 141 | // | ParallelForEach | 10000 | 8 | 213.479 us | 3.7448 us | 3.5029 us | 1.4648 | 3.16 KB | 142 | // | ParallelForEach | 160000 | 1 | 14,010.347 us | 4.7996 us | 4.2547 us | - | 1.84 KB | 143 | // | ParallelForEach | 160000 | 2 | 7,091.299 us | 3.7071 us | 3.4676 us | - | 1.98 KB | 144 | // | ParallelForEach | 160000 | 4 | 4,190.386 us | 35.1721 us | 32.9000 us | - | 2.38 KB | 145 | // | ParallelForEach | 160000 | 8 | 3,395.585 us | 39.0934 us | 36.5680 us | - | 3.19 KB | 146 | // | | | | | | | | | 147 | // | AsParallel | 10 | 1 | 1.657 us | 0.0044 us | 0.0037 us | 1.0738 | 2.2 KB | 148 | // | AsParallel | 10 | 2 | 3.066 us | 0.0604 us | 0.1105 us | 1.2360 | 2.5 KB | 149 | // | AsParallel | 10 | 4 | 5.902 us | 0.0624 us | 0.0584 us | 1.5411 | 3.11 KB | 150 | // | AsParallel | 10 | 8 | 7.994 us | 0.1596 us | 0.2878 us | 2.1515 | 4.33 KB | 151 | // | AsParallel | 10000 | 1 | 867.189 us | 1.2422 us | 1.1619 us | 0.9766 | 2.2 KB | 152 | // | AsParallel | 10000 | 2 | 448.982 us | 1.1591 us | 0.9679 us | 0.9766 | 2.51 KB | 153 | // | AsParallel | 10000 | 4 | 298.986 us | 1.7944 us | 1.5907 us | 1.4648 | 3.15 KB | 154 | // | AsParallel | 10000 | 8 | 325.605 us | 4.6200 us | 4.0955 us | 1.9531 | 4.38 KB | 155 | // | AsParallel | 160000 | 1 | 13,847.851 us | 2.9534 us | 2.6181 us | - | 2.21 KB | 156 | // | AsParallel | 160000 | 2 | 7,085.316 us | 9.1999 us | 8.6056 us | - | 2.53 KB | 157 | // | AsParallel | 160000 | 4 | 4,282.067 us | 27.6395 us | 25.8540 us | - | 3.15 KB | 158 | // | AsParallel | 160000 | 8 | 3,722.779 us | 29.9159 us | 24.9811 us | - | 4.39 KB | 159 | // | | | | | | | | | 160 | // | AsParallelAsync | 10 | 1 | 3.058 us | 0.0328 us | 0.0307 us | 0.5836 | 1.19 KB | 161 | // | AsParallelAsync | 10 | 2 | 6.804 us | 0.1261 us | 0.2460 us | 0.7477 | 1.5 KB | 162 | // | AsParallelAsync | 10 | 4 | 8.003 us | 0.1272 us | 0.1514 us | 1.1444 | 2.3 KB | 163 | // | AsParallelAsync | 10 | 8 | 11.815 us | 0.1530 us | 0.1431 us | 1.9531 | 3.91 KB | 164 | // | AsParallelAsync | 10000 | 1 | 870.795 us | 3.5375 us | 2.9539 us | - | 1.25 KB | 165 | // | AsParallelAsync | 10000 | 2 | 453.632 us | 6.2659 us | 8.5769 us | - | 1.56 KB | 166 | // | AsParallelAsync | 10000 | 4 | 558.011 us | 21.1598 us | 62.3901 us | 0.9766 | 2.3 KB | 167 | // | AsParallelAsync | 10000 | 8 | 188.933 us | 2.7895 us | 2.6093 us | 1.7090 | 3.72 KB | 168 | // | AsParallelAsync | 160000 | 1 | 13,805.764 us | 96.9952 us | 85.9837 us | - | 1.27 KB | 169 | // | AsParallelAsync | 160000 | 2 | 6,960.659 us | 21.6161 us | 19.1621 us | - | 1.57 KB | 170 | // | AsParallelAsync | 160000 | 4 | 4,010.717 us | 20.6152 us | 19.2835 us | - | 2.31 KB | 171 | // | AsParallelAsync | 160000 | 8 | 2,970.931 us | 43.2653 us | 40.4704 us | - | 3.76 KB | 172 | // | | | | | | | | | 173 | // | AsParallelLazyAsync | 10 | 1 | 3.356 us | 0.0483 us | 0.0452 us | 0.6676 | 1.35 KB | 174 | // | AsParallelLazyAsync | 10 | 2 | 7.010 us | 0.1393 us | 0.3597 us | 0.9537 | 1.91 KB | 175 | // | AsParallelLazyAsync | 10 | 4 | 8.501 us | 0.1364 us | 0.1276 us | 1.6022 | 3.21 KB | 176 | // | AsParallelLazyAsync | 10 | 8 | 12.848 us | 0.2559 us | 0.3671 us | 2.8992 | 5.81 KB | 177 | // | AsParallelLazyAsync | 10000 | 1 | 1,036.081 us | 0.9934 us | 0.9292 us | - | 1.42 KB | 178 | // | AsParallelLazyAsync | 10000 | 2 | 537.028 us | 1.1429 us | 1.0690 us | 0.9766 | 1.98 KB | 179 | // | AsParallelLazyAsync | 10000 | 4 | 605.514 us | 11.5874 us | 13.3441 us | 0.9766 | 3.21 KB | 180 | // | AsParallelLazyAsync | 10000 | 8 | 255.192 us | 4.9633 us | 7.7272 us | 2.4414 | 5.63 KB | 181 | // | AsParallelLazyAsync | 160000 | 1 | 16,391.756 us | 22.8568 us | 21.3803 us | - | 1.45 KB | 182 | // | AsParallelLazyAsync | 160000 | 2 | 8,374.102 us | 11.9937 us | 11.2189 us | - | 2 KB | 183 | // | AsParallelLazyAsync | 160000 | 4 | 5,374.254 us | 34.9108 us | 32.6556 us | - | 3.22 KB | 184 | // | AsParallelLazyAsync | 160000 | 8 | 3,932.218 us | 29.3498 us | 27.4538 us | - | 5.72 KB | 185 | // 186 | // 187 | // 188 | // | Method | ListLength | ThreadsCount | Mean | Error | StdDev | Gen0 | Allocated | 189 | // |-------------------- |----------- |------------- |--------------:|-----------:|-----------:|-------:|----------:| 190 | // | AsParallelLazyAsync | 10 | 1 | 2.615 us | 0.0433 us | 0.0362 us | 0.6638 | 1.35 KB | 191 | // | AsParallelLazyAsync | 10 | 8 | 41.348 us | 2.4909 us | 7.2659 us | 2.9297 | 5.8 KB | 192 | // | AsParallelLazyAsync | 160000 | 1 | 13,590.928 us | 46.4101 us | 41.1414 us | - | 1.43 KB | 193 | // | AsParallelLazyAsync | 160000 | 8 | 2,382.634 us | 19.7945 us | 18.5158 us | - | 5.68 KB | 194 | // 195 | // */ 196 | -------------------------------------------------------------------------------- /AwaitThreading.Core.Tests/TaskOverParallelTaskTests.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // Copyright (c) 2024 Saltuk Konstantin 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using AwaitThreading.Core.Context; 6 | using AwaitThreading.Core.Operations; 7 | using AwaitThreading.Core.Tasks; 8 | 9 | #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously 10 | namespace AwaitThreading.Core.Tests; 11 | 12 | [TestFixture] 13 | public class TaskOverParallelTaskTests : BaseClassWithParallelContextValidation 14 | { 15 | [Test] 16 | public async Task Await_ParallelTaskIsSimple_Success() 17 | { 18 | var result = await TestBody(); 19 | Assert.That(result, Is.EqualTo(42)); 20 | return; 21 | 22 | async ParallelTask TestBody() 23 | { 24 | return 42; 25 | } 26 | } 27 | 28 | [Test] 29 | public async Task Await_ParallelTaskHasPairedForkAndJoin_Success() 30 | { 31 | var result = await TestBody(); 32 | Assert.That(result, Is.EqualTo(42)); 33 | return; 34 | 35 | async ParallelTask TestBody() 36 | { 37 | await new ForkingTask(2); 38 | await new JoiningTask(); 39 | return 42; 40 | } 41 | } 42 | 43 | [Test] 44 | public async Task Await_ParallelTaskThrowsException_ExceptionIsPropagated() 45 | { 46 | await AssertEx.CheckThrowsAsync(TestBody); 47 | return; 48 | 49 | async Task TestBody() 50 | { 51 | await TestBodyInner(); 52 | } 53 | 54 | async ParallelTask TestBodyInner() 55 | { 56 | throw new AssertionException("Message"); 57 | } 58 | } 59 | 60 | [TestCase(true)] 61 | [TestCase(false)] 62 | public async Task Await_ParallelTaskHasUnpairedFork_InvalidOperationExceptionIsThrows(bool inSyncPart) 63 | { 64 | await AssertEx.CheckThrowsAsync(TestBody); 65 | return; 66 | 67 | async Task TestBody() 68 | { 69 | if (!inSyncPart) 70 | { 71 | await Task.Yield(); 72 | } 73 | 74 | await TestBodyInner(); 75 | } 76 | 77 | async ParallelTask TestBodyInner() 78 | { 79 | await new ForkingTask(2); 80 | } 81 | } 82 | 83 | [Test, Ignore("AsyncTaskMethodBuilder propagate exception to thread pool, can't handle it in test")] 84 | public async Task Await_DirectFork_InvalidOperationExceptionIsThrows() 85 | { 86 | await AssertEx.CheckThrowsAsync(TestBody); 87 | return; 88 | 89 | async Task TestBody() 90 | { 91 | await new ForkingTask(2); 92 | } 93 | } 94 | 95 | [Test, Ignore("AsyncTaskMethodBuilder propagate exception to thread pool, can't handle it in test")] 96 | public async Task Await_DirectJoin_InvalidOperationExceptionIsThrows() 97 | { 98 | await AssertEx.CheckThrowsAsync(TestBody); 99 | return; 100 | 101 | async Task TestBody() 102 | { 103 | await new JoiningTask(); 104 | } 105 | } 106 | 107 | [Test, Ignore("AsyncTaskMethodBuilder propagate exception to thread pool, can't handle it in test")] 108 | public async Task Await_ParallelTaskHasDirectJoin_InvalidOperationExceptionIsThrows() 109 | { 110 | await AssertEx.CheckThrowsAsync(TestBody); 111 | return; 112 | 113 | async Task TestBody() 114 | { 115 | await ParallelTaskBody(); 116 | } 117 | 118 | async ParallelTask ParallelTaskBody() 119 | { 120 | await new ForkingTask(2); 121 | await StandardTaskMethod(); 122 | } 123 | 124 | async Task StandardTaskMethod() 125 | { 126 | await new JoiningTask(); 127 | } 128 | } 129 | 130 | [Test] 131 | public async Task Await_ParallelTaskHasUnpairedJoin_InvalidOperationExceptionIsThrows() 132 | { 133 | await AssertEx.CheckThrowsAsync(TestBody); 134 | return; 135 | 136 | async Task TestBody() 137 | { 138 | await ParallelTaskBody(); 139 | } 140 | 141 | async ParallelTask ParallelTaskBody() 142 | { 143 | if (ParallelContextStorage.CurrentThreadContext.IsEmpty is false) 144 | { 145 | FailFast(); 146 | } 147 | 148 | await new ForkingTask(2); 149 | 150 | if (ParallelContextStorage.CurrentThreadContext.IsEmpty) 151 | { 152 | FailFast(); 153 | } 154 | 155 | var standardTaskMethod = StandardTaskMethod(); 156 | try 157 | { 158 | await standardTaskMethod; 159 | } 160 | finally 161 | { 162 | if (ParallelContextStorage.CurrentThreadContext.IsEmpty) 163 | { 164 | FailFast(); 165 | } 166 | } 167 | 168 | return 1; 169 | } 170 | 171 | async Task StandardTaskMethod() 172 | { 173 | // Note: just explicitly check current behavior. In general, we would like to have an empty context here, 174 | // but it's not possible, since we do not have a control over sync execution 175 | if (ParallelContextStorage.CurrentThreadContext.IsEmpty) 176 | { 177 | FailFast(); 178 | } 179 | 180 | try 181 | { 182 | var innerParallelTaskBody = InnerParallelTaskBody(); 183 | await innerParallelTaskBody; 184 | } 185 | finally 186 | { 187 | // NOTE: here we need to have an EMPTY context. Otherwise, standard TaskMethodBuilder can return 188 | // the thread with ParallelContext to the thread pool, since continuation is likely to be executed 189 | // on another thread, and we have no more control after over it. 190 | if (ParallelContextStorage.CurrentThreadContext.IsEmpty is false) 191 | { 192 | FailFast(); 193 | } 194 | } 195 | } 196 | 197 | async ParallelTask InnerParallelTaskBody() 198 | { 199 | if (ParallelContextStorage.CurrentThreadContext.IsEmpty is false) 200 | { 201 | FailFast(); 202 | } 203 | 204 | await new JoiningTask(); 205 | } 206 | } 207 | 208 | [TestCase(1)] 209 | [TestCase(2)] 210 | public async Task Await_RegularTaskIsAwaited_ContextIsClearedAndRestored1(int threadCount) 211 | { 212 | await TestBody(); 213 | 214 | async ParallelTask TestBody() 215 | { 216 | await new ForkingTask(threadCount); 217 | var regularAsyncMethod = RegularAsyncMethod(); 218 | await regularAsyncMethod; 219 | 220 | Assert.That(ParallelContextStorage.CurrentThreadContext.IsEmpty, Is.False); 221 | await new JoiningTask(); 222 | } 223 | 224 | async Task RegularAsyncMethod() 225 | { 226 | // Note: just explicitly check current behavior. In general, we would like to have an empty context here, 227 | // but it's not possible, since we do not have a control over sync execution 228 | Assert.That(ParallelContextStorage.CurrentThreadContext.IsEmpty, Is.False); 229 | 230 | await Task.Yield(); 231 | 232 | Assert.That(ParallelContextStorage.CurrentThreadContext.IsEmpty, Is.True); 233 | } 234 | } 235 | 236 | [TestCase(1)] 237 | [TestCase(2)] 238 | public async Task Await_RegularTaskIsAwaited_ContextIsClearedAndRestored2(int threadCount) 239 | { 240 | await TestBody(); 241 | 242 | async ParallelTask TestBody() 243 | { 244 | await new ForkingTask(threadCount); 245 | var regularAsyncMethod = RegularAsyncMethod(); 246 | await regularAsyncMethod; 247 | 248 | Assert.That(ParallelContextStorage.CurrentThreadContext.IsEmpty, Is.False); 249 | await new JoiningTask(); 250 | } 251 | 252 | async Task RegularAsyncMethod() 253 | { 254 | // Note: just explicitly check current behavior. In general, we would like to have an empty context here, 255 | // but it's not possible, since we do not have a control over sync execution 256 | Assert.That(ParallelContextStorage.CurrentThreadContext.IsEmpty, Is.False); 257 | 258 | await ParallelMethod(); 259 | 260 | Assert.That(ParallelContextStorage.CurrentThreadContext.IsEmpty, Is.True); 261 | } 262 | 263 | async ParallelTask ParallelMethod() 264 | { 265 | await new ForkingTask(threadCount); 266 | await new JoiningTask(); 267 | } 268 | } 269 | 270 | [TestCase(1)] 271 | [TestCase(2)] 272 | public async Task Await_RegularTaskIsAwaited_ContextIsClearedAndRestored3(int threadCount) 273 | { 274 | await TestBody(); 275 | 276 | async ParallelTask TestBody() 277 | { 278 | await new ForkingTask(threadCount); 279 | var regularAsyncMethod = RegularAsyncMethod(); 280 | await regularAsyncMethod; 281 | 282 | Assert.That(ParallelContextStorage.CurrentThreadContext.IsEmpty, Is.False); 283 | await new JoiningTask(); 284 | } 285 | 286 | async Task RegularAsyncMethod() 287 | { 288 | // Note: just explicitly check current behavior. In general, we would like to have an empty context here, 289 | // but it's not possible, since we do not have a control over sync execution 290 | Assert.That(ParallelContextStorage.CurrentThreadContext.IsEmpty, Is.False); 291 | 292 | await ParallelMethod(); 293 | 294 | Assert.That(ParallelContextStorage.CurrentThreadContext.IsEmpty, Is.True); 295 | } 296 | 297 | async ParallelTask ParallelMethod() 298 | { 299 | await new ForkingTask(threadCount); 300 | await new JoiningTask(); 301 | 302 | try 303 | { 304 | await new JoiningTask(); 305 | } 306 | catch (InvalidOperationException) 307 | { 308 | } 309 | } 310 | } 311 | 312 | [TestCase(1)] 313 | [TestCase(2)] 314 | public async Task Await_RegularTaskIsAwaited_ContextIsClearedAndRestored4(int threadCount) 315 | { 316 | await TestBody(); 317 | 318 | async ParallelTask TestBody() 319 | { 320 | await new ForkingTask(threadCount); 321 | var regularAsyncMethod = RegularAsyncMethod(); 322 | await regularAsyncMethod; 323 | 324 | Assert.That(ParallelContextStorage.CurrentThreadContext.IsEmpty, Is.False); 325 | await new JoiningTask(); 326 | } 327 | 328 | async Task RegularAsyncMethod() 329 | { 330 | // Note: just explicitly check current behavior. In general, we would like to have an empty context here, 331 | // but it's not possible, since we do not have a control over sync execution 332 | Assert.That(ParallelContextStorage.CurrentThreadContext.IsEmpty, Is.False); 333 | 334 | 335 | try 336 | { 337 | await ParallelMethod(); 338 | } 339 | catch (InvalidOperationException) 340 | { 341 | } 342 | 343 | Assert.That(ParallelContextStorage.CurrentThreadContext.IsEmpty, Is.True); 344 | } 345 | 346 | async ParallelTask ParallelMethod() 347 | { 348 | await new ForkingTask(threadCount); 349 | } 350 | } 351 | } -------------------------------------------------------------------------------- /AwaitThreading.Enumerable/ParallelRangeManager.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | // Changes made to this file: 5 | // - Added support for empty ranges on 2024-10-09 by Konstantin Saltuk 6 | 7 | // =+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ 8 | // 9 | // Implements the algorithm for distributing loop indices to parallel loop workers 10 | // 11 | // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 12 | 13 | // ReSharper disable All 14 | 15 | using System.Diagnostics; 16 | using System.Numerics; 17 | using System.Runtime.CompilerServices; 18 | using System.Runtime.InteropServices; 19 | 20 | namespace System.Threading.Tasks 21 | { 22 | /// 23 | /// Represents an index range 24 | /// 25 | [StructLayout(LayoutKind.Auto)] 26 | internal struct IndexRange 27 | { 28 | // the From and To values for this range. These do not change. 29 | internal long _nFromInclusive; 30 | internal long _nToExclusive; 31 | 32 | // The shared index, stored as the offset from nFromInclusive. Using an offset rather than the actual 33 | // value saves us from overflows that can happen due to multiple workers racing to increment this. 34 | // All updates to this field need to be interlocked. To avoid split interlockeds across cache-lines 35 | // in 32-bit processes, in 32-bit processes when the range fits in a 32-bit value, we prefer to use 36 | // a 32-bit field, and just use the first 32-bits of the long. And to minimize false sharing, each 37 | // value is stored in its own heap-allocated object, which is lazily allocated by the thread using 38 | // that range, minimizing the chances it'll be near the objects from other threads. 39 | internal volatile StrongBox? _nSharedCurrentIndexOffset; 40 | 41 | // to be set to 1 by the worker that finishes this range. It's OK to do a non-interlocked write here. 42 | internal int _bRangeFinished; 43 | } 44 | 45 | 46 | /// 47 | /// The RangeWorker struct wraps the state needed by a task that services the parallel loop 48 | /// 49 | [StructLayout(LayoutKind.Auto)] 50 | internal struct RangeWorker 51 | { 52 | // reference to the IndexRange array allocated by the range manager 53 | internal readonly IndexRange[] _indexRanges; 54 | 55 | // index of the current index range that this worker is grabbing chunks from 56 | internal int _nCurrentIndexRange; 57 | 58 | // the step for this loop. Duplicated here for quick access (rather than jumping to rangemanager) 59 | internal long _nStep; 60 | 61 | // increment value is the current amount that this worker will use 62 | // to increment the shared index of the range it's working on 63 | internal long _nIncrementValue; 64 | 65 | // the increment value is doubled each time this worker finds work, and is capped at this value 66 | internal readonly long _nMaxIncrementValue; 67 | 68 | // whether to use 32-bits or 64-bits of current index in each range 69 | internal readonly bool _use32BitCurrentIndex; 70 | 71 | internal bool IsInitialized { get { return _indexRanges != null; } } 72 | 73 | /// 74 | /// Initializes a RangeWorker struct 75 | /// 76 | internal RangeWorker(IndexRange[] ranges, int nInitialRange, long nStep, bool use32BitCurrentIndex) 77 | { 78 | _indexRanges = ranges; 79 | _use32BitCurrentIndex = use32BitCurrentIndex; 80 | _nCurrentIndexRange = nInitialRange; 81 | _nStep = nStep; 82 | 83 | _nIncrementValue = nStep; 84 | 85 | _nMaxIncrementValue = 16 * nStep; 86 | } 87 | 88 | /// 89 | /// Implements the core work search algorithm that will be used for this range worker. 90 | /// 91 | /// 92 | /// Usage pattern is: 93 | /// 1) the thread associated with this rangeworker calls FindNewWork 94 | /// 2) if we return true, the worker uses the nFromInclusiveLocal and nToExclusiveLocal values 95 | /// to execute the sequential loop 96 | /// 3) if we return false it means there is no more work left. It's time to quit. 97 | /// 98 | private bool FindNewWork(out long nFromInclusiveLocal, out long nToExclusiveLocal) 99 | { 100 | // since we iterate over index ranges circularly, we will use the 101 | // count of visited ranges as our exit condition 102 | int numIndexRangesToVisit = _indexRanges.Length; 103 | 104 | do 105 | { 106 | // local snap to save array access bounds checks in places where we only read fields 107 | IndexRange currentRange = _indexRanges[_nCurrentIndexRange]; 108 | 109 | if (currentRange._bRangeFinished == 0) 110 | { 111 | StrongBox? sharedCurrentIndexOffset = _indexRanges[_nCurrentIndexRange]._nSharedCurrentIndexOffset; 112 | if (sharedCurrentIndexOffset == null) 113 | { 114 | Interlocked.CompareExchange(ref _indexRanges[_nCurrentIndexRange]._nSharedCurrentIndexOffset, new StrongBox(0), null); 115 | sharedCurrentIndexOffset = _indexRanges[_nCurrentIndexRange]._nSharedCurrentIndexOffset!; 116 | } 117 | 118 | long nMyOffset; 119 | if (IntPtr.Size == 4 && _use32BitCurrentIndex) 120 | { 121 | // In 32-bit processes, we prefer to use 32-bit interlocked operations, to avoid the possibility of doing 122 | // a 64-bit interlocked when the target value crosses a cache line, as that can be super expensive. 123 | // We use the first 32 bits of the Int64 index in such cases. 124 | unsafe 125 | { 126 | fixed (long* indexPtr = &sharedCurrentIndexOffset.Value) 127 | { 128 | nMyOffset = Interlocked.Add(ref *(int*)indexPtr, (int)_nIncrementValue) - _nIncrementValue; 129 | } 130 | } 131 | } 132 | else 133 | { 134 | nMyOffset = Interlocked.Add(ref sharedCurrentIndexOffset.Value, _nIncrementValue) - _nIncrementValue; 135 | } 136 | 137 | if (currentRange._nToExclusive - currentRange._nFromInclusive > nMyOffset) 138 | { 139 | // we found work 140 | 141 | nFromInclusiveLocal = currentRange._nFromInclusive + nMyOffset; 142 | nToExclusiveLocal = unchecked(nFromInclusiveLocal + _nIncrementValue); 143 | 144 | // Check for going past end of range, or wrapping 145 | if ((nToExclusiveLocal > currentRange._nToExclusive) || (nToExclusiveLocal < currentRange._nFromInclusive)) 146 | { 147 | nToExclusiveLocal = currentRange._nToExclusive; 148 | } 149 | 150 | // We will double our unit of increment until it reaches the maximum. 151 | if (_nIncrementValue < _nMaxIncrementValue) 152 | { 153 | _nIncrementValue *= 2; 154 | if (_nIncrementValue > _nMaxIncrementValue) 155 | { 156 | _nIncrementValue = _nMaxIncrementValue; 157 | } 158 | } 159 | 160 | return true; 161 | } 162 | else 163 | { 164 | // this index range is completed, mark it so that others can skip it quickly 165 | Interlocked.Exchange(ref _indexRanges[_nCurrentIndexRange]._bRangeFinished, 1); 166 | } 167 | } 168 | 169 | // move on to the next index range, in circular order. 170 | _nCurrentIndexRange = (_nCurrentIndexRange + 1) % _indexRanges.Length; 171 | numIndexRangesToVisit--; 172 | } while (numIndexRangesToVisit > 0); 173 | // we've visited all index ranges possible => there's no work remaining 174 | 175 | nFromInclusiveLocal = 0; 176 | nToExclusiveLocal = 0; 177 | 178 | return false; 179 | } 180 | 181 | internal bool FindNewWork(out int fromInclusive, out int toExclusive) 182 | { 183 | bool success = FindNewWork(out long fromInclusiveInt64, out long toExclusiveInt64); 184 | 185 | fromInclusive = (int)(fromInclusiveInt64); 186 | toExclusive = (int)(toExclusiveInt64); 187 | 188 | return success; 189 | } 190 | } 191 | 192 | 193 | /// 194 | /// Represents the entire loop operation, keeping track of workers and ranges. 195 | /// 196 | /// 197 | /// The usage pattern is: 198 | /// 1) The Parallel loop entry function (ForWorker) creates an instance of this class 199 | /// 2) Every thread joining to service the parallel loop calls RegisterWorker to grab a 200 | /// RangeWorker struct to wrap the state it will need to find and execute work, 201 | /// and they keep interacting with that struct until the end of the loop 202 | internal sealed class RangeManager 203 | { 204 | internal readonly IndexRange[] _indexRanges; 205 | internal readonly bool _use32BitCurrentIndex; 206 | 207 | internal int _nCurrentIndexRangeToAssign; 208 | internal long _nStep; 209 | 210 | /// 211 | /// Initializes a RangeManager with the given loop parameters, and the desired number of outer ranges 212 | /// 213 | internal RangeManager(long nFromInclusive, long nToExclusive, long nStep, int nNumExpectedWorkers) 214 | { 215 | if (nNumExpectedWorkers <= 0) 216 | { 217 | throw new ArgumentOutOfRangeException(nameof(nNumExpectedWorkers), $"{nameof(nNumExpectedWorkers)} must be positive"); 218 | } 219 | 220 | _nCurrentIndexRangeToAssign = 0; 221 | _nStep = nStep; 222 | 223 | // Our signed math breaks down w/ nNumExpectedWorkers == 1. So change it to 2. 224 | if (nNumExpectedWorkers == 1) 225 | nNumExpectedWorkers = 2; 226 | 227 | // 228 | // calculate the size of each index range 229 | // 230 | 231 | ulong uSpan = (ulong)(nToExclusive - nFromInclusive); 232 | ulong uRangeSize = uSpan / (ulong)nNumExpectedWorkers; // rough estimate first 233 | 234 | uRangeSize -= uRangeSize % (ulong)nStep; // snap to multiples of nStep 235 | // otherwise index range transitions will derail us from nStep 236 | 237 | if (uRangeSize == 0) 238 | { 239 | uRangeSize = (ulong)nStep; 240 | } 241 | 242 | // 243 | // find the actual number of index ranges we will need 244 | // 245 | Debug.Assert((uSpan / uRangeSize) < int.MaxValue); 246 | 247 | int nNumRanges = (int)(uSpan / uRangeSize); 248 | 249 | if (uSpan % uRangeSize != 0) 250 | { 251 | nNumRanges++; 252 | } 253 | 254 | 255 | // Convert to signed so the rest of the logic works. 256 | // Should be fine so long as uRangeSize < Int64.MaxValue, which we guaranteed by setting #workers >= 2. 257 | long nRangeSize = (long)uRangeSize; 258 | _use32BitCurrentIndex = IntPtr.Size == 4 && nRangeSize <= int.MaxValue; 259 | 260 | if (nNumRanges == 0) 261 | { 262 | _indexRanges = new[] { new IndexRange() { _bRangeFinished = 1 } }; 263 | return; 264 | } 265 | 266 | // allocate the array of index ranges 267 | _indexRanges = new IndexRange[nNumRanges]; 268 | 269 | long nCurrentIndex = nFromInclusive; 270 | for (int i = 0; i < nNumRanges; i++) 271 | { 272 | // the fromInclusive of the new index range is always on nCurrentIndex 273 | _indexRanges[i]._nFromInclusive = nCurrentIndex; 274 | _indexRanges[i]._nSharedCurrentIndexOffset = null; 275 | _indexRanges[i]._bRangeFinished = 0; 276 | 277 | // now increment it to find the toExclusive value for our range 278 | nCurrentIndex = unchecked(nCurrentIndex + nRangeSize); 279 | 280 | // detect integer overflow or range overage and snap to nToExclusive 281 | if (nCurrentIndex < unchecked(nCurrentIndex - nRangeSize) || 282 | nCurrentIndex > nToExclusive) 283 | { 284 | // this should only happen at the last index 285 | Debug.Assert(i == nNumRanges - 1); 286 | 287 | nCurrentIndex = nToExclusive; 288 | } 289 | 290 | // now that the end point of the new range is calculated, assign it. 291 | _indexRanges[i]._nToExclusive = nCurrentIndex; 292 | } 293 | } 294 | 295 | /// 296 | /// The function that needs to be called by each new worker thread servicing the parallel loop 297 | /// in order to get a RangeWorker struct that wraps the state for finding and executing indices 298 | /// 299 | internal RangeWorker RegisterNewWorker() 300 | { 301 | Debug.Assert(_indexRanges != null && _indexRanges.Length != 0); 302 | 303 | int nInitialRange = (Interlocked.Increment(ref _nCurrentIndexRangeToAssign) - 1) % _indexRanges.Length; 304 | 305 | return new RangeWorker(_indexRanges, nInitialRange, _nStep, _use32BitCurrentIndex); 306 | } 307 | } 308 | } 309 | --------------------------------------------------------------------------------