├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── dependabot.yml └── workflows │ └── dotnetcore.yml ├── global.json ├── src └── CSRakowski.Parallel │ ├── CSRakowski.Parallel.snk │ ├── Extensions │ ├── IParallelAsyncEnumerable.cs │ ├── ParallelAsyncEx.AsyncStreams.cs │ ├── ParallelAsyncEnumerable.cs │ └── ParallelAsyncEx.cs │ ├── CSRakowski.Parallel.csproj │ ├── Helpers │ ├── ListHelpers.cs │ └── ParallelAsyncEventSource.cs │ ├── ParallelAsync.Unordered.cs │ ├── ParallelAsync.Unbatched.cs │ └── ParallelAsync.Ordered.cs ├── SECURITY.md ├── tests ├── CSRakowski.Parallel.Benchmarks │ ├── Program.cs │ ├── CSRakowski.Parallel.Benchmarks.csproj │ ├── TestFunctions.cs │ └── Benchmarks │ │ ├── Computations.cs │ │ ├── ParallelAsyncTestBenchmarks.cs │ │ ├── FuncOverloading.cs │ │ ├── ParallelAsyncBenchmarks.cs │ │ ├── ParallelAsyncBenchmarks_IAsyncEnumerable.cs │ │ ├── ParallelAsyncBenchmarks_AsyncStreams.cs │ │ ├── UsingForEachForUnbatched.cs │ │ ├── UsingForEachForOrdered.cs │ │ └── CompareWith_Parallel_ForEachAsync.cs ├── Profiling │ ├── App.config │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Program.cs │ └── Profiling.csproj └── CSRakowski.Parallel.Tests │ ├── Mocks and Helpers │ └── TestCollections.cs │ ├── CSRakowski.Parallel.Tests.csproj │ ├── HelpersTests.cs │ ├── ExtensionMethodsTests_AsyncStreams.cs │ ├── ExtensionMethodsTests_IAsyncEnumerable.cs │ ├── ExtensionMethodsTests.cs │ ├── ParallelAsyncTests_AsyncStreams.cs │ ├── ParallelAsyncTests.cs │ └── ParallelAsyncTests_IAsyncEnumerable.cs ├── LICENSE ├── Directory.Build.props ├── README.md ├── CSRakowski.ParallelAsync.sln └── .gitignore /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: csrakowski 4 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | //"version": "9.0.300", 4 | "rollForward": "latestFeature" 5 | } 6 | } -------------------------------------------------------------------------------- /src/CSRakowski.Parallel/CSRakowski.Parallel.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csrakowski/ParallelAsync/HEAD/src/CSRakowski.Parallel/CSRakowski.Parallel.snk -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | _Summary of the issue you are experiencing, or the changes you would like to propose_ 4 | 5 | 6 | ### Sample 7 | 8 | ```cs 9 | 10 | 11 | ``` 12 | 13 | ### Details 14 | 15 | - Operating system: 16 | - .NET Runtime: 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "06:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: "xunit.runner.visualstudio" 11 | update-types: ["version-update:semver-minor"] 12 | -------------------------------------------------------------------------------- /src/CSRakowski.Parallel/Extensions/IParallelAsyncEnumerable.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace CSRakowski.Parallel.Extensions 4 | { 5 | /// 6 | /// Empty marker interface, used by the 7 | /// 8 | /// The element type 9 | public interface IParallelAsyncEnumerable : IEnumerable 10 | { 11 | } 12 | } -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | If you discover a security vulnerability in this project, please report it responsibly: 6 | 7 | - Create a public issue in the project's issue tracker with a basic highlevel description to notify the maintainers. 8 | - **Do Not** disclose any sensitive details in the issue. 9 | - Leave contact information in the issue so the maintainers can reach you for more details. 10 | - You will receive a response as soon as possible, typically within 7 days. 11 | - After the issue is resolved, you may be credited in the release notes if you wish. 12 | 13 | We appreciate your help in keeping this project and its users safe! 14 | -------------------------------------------------------------------------------- /tests/CSRakowski.Parallel.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using BenchmarkDotNet; 9 | using BenchmarkDotNet.Running; 10 | using BenchmarkDotNet.Reports; 11 | using BenchmarkDotNet.Attributes; 12 | 13 | namespace CSRakowski.Parallel.Benchmarks 14 | { 15 | public static class Program 16 | { 17 | public static void Main(string[] args) 18 | { 19 | var summary = BenchmarkRunner.Run(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/CSRakowski.Parallel.Benchmarks/CSRakowski.Parallel.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net48;net472;net90;net80;net60; 5 | Exe 6 | false 7 | 8 | CSRakowski.Parallel.Benchmarks 9 | CSRakowski.Parallel.Benchmarks 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2025 Christiaan Rakowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/Profiling/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | Christiaan Rakowski 4 | Christiaan Rakowski - 2017-2025 5 | 1.8.0 6 | latest 7 | 8 | 9 | 10 | true 11 | portable 12 | true 13 | snupkg 14 | 15 | 16 | 17 | false 18 | 19 | 20 | 21 | true 22 | true 23 | 24 | 25 | 26 | OS_WINDOWS 27 | 28 | 29 | OS_LINUX 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/CSRakowski.Parallel.Tests/Mocks and Helpers/TestCollections.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace CSRakowski.Parallel.Tests.Helpers 9 | { 10 | internal class TestCollection : IEnumerable, ICollection 11 | { 12 | private readonly int _size; 13 | 14 | public TestCollection(int size) 15 | { 16 | _size = size; 17 | } 18 | 19 | #region ICollection 20 | 21 | public int Count => _size; 22 | 23 | public Object SyncRoot => this; 24 | public bool IsSynchronized => true; 25 | 26 | public void CopyTo(Array array, int index) { } 27 | 28 | IEnumerator IEnumerable.GetEnumerator() => null; 29 | 30 | public IEnumerator GetEnumerator() => null; 31 | 32 | #endregion ICollection 33 | 34 | } 35 | 36 | internal class TestReadOnlyCollection : IReadOnlyCollection 37 | { 38 | private readonly int _size; 39 | 40 | public TestReadOnlyCollection(int size) 41 | { 42 | _size = size; 43 | } 44 | 45 | #region IReadOnlyCollection 46 | 47 | public int Count => _size; 48 | 49 | IEnumerator IEnumerable.GetEnumerator() => null; 50 | 51 | public IEnumerator GetEnumerator() => null; 52 | 53 | #endregion IReadOnlyCollection 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/CSRakowski.Parallel.Tests/CSRakowski.Parallel.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net48;net472;net90;net80;net60 5 | 6 | false 7 | true 8 | 9 | CSRakowski.Parallel.Tests 10 | CSRakowski.Parallel.Tests 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/Profiling/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Profiling")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Profiling")] 13 | [assembly: AssemblyCopyright("Copyright © Christiaan Rakowski - 2018-2022")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("695e8128-39a1-4c05-b182-e6fd18786b0f")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /tests/CSRakowski.Parallel.Benchmarks/TestFunctions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace CSRakowski.Parallel.Benchmarks 9 | { 10 | public static class TestFunctions 11 | { 12 | public static Task JustAddOne(int number) 13 | { 14 | return Task.FromResult(number + 1); 15 | } 16 | 17 | public static Task JustAddOne_WithCancellationToken(int number, CancellationToken cancellationToken) 18 | { 19 | return Task.FromResult(number + 1); 20 | } 21 | 22 | public static Task ReturnCompletedTask(int number) 23 | { 24 | return Task.CompletedTask; 25 | } 26 | 27 | public static Task ReturnCompletedTask_WithCancellationToken(int number, CancellationToken cancellationToken) 28 | { 29 | return Task.CompletedTask; 30 | } 31 | 32 | public static Task Compute_Double(int number) 33 | { 34 | var cosh = Math.Cosh(number); 35 | var sinh = Math.Sinh(number); 36 | 37 | var base16 = Math.Log(number, 16); 38 | var cosh16 = Math.Cosh(base16); 39 | var sinh16 = Math.Sinh(base16); 40 | 41 | double result = number; 42 | result *= cosh; 43 | result *= sinh; 44 | result *= base16; 45 | result *= cosh16; 46 | result *= sinh16; 47 | 48 | return Task.FromResult(result); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/CSRakowski.Parallel/CSRakowski.Parallel.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net80;net472;netstandard2.0 5 | true 6 | 7 | CSRakowski.ParallelAsync 8 | CSRakowski.ParallelAsync 9 | A .NET utility library for running async methods in parallel batches 10 | https://github.com/csrakowski/ParallelAsync 11 | LICENSE 12 | README.md 13 | https://github.com/csrakowski/ParallelAsync 14 | Git 15 | Parallel, Async, Batching 16 | true 17 | CSRakowski.Parallel.snk 18 | * Updated TargetFrameworks to remove old unsupported ones. 19 | 20 | CSRakowski.Parallel 21 | CSRakowski.Parallel 22 | 23 | 24 | 25 | bin\$(Configuration)\$(TargetFramework)\CSRakowski.Parallel.xml 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/Profiling/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using CSRakowski.Parallel; 8 | 9 | namespace Profiling 10 | { 11 | public static class Program 12 | { 13 | public static async Task Main(string[] args) 14 | { 15 | const int numberOfElements = 1000000; 16 | const int batchSize = 64; 17 | const bool outOfOrder = false; 18 | var input = Enumerable.Range(1, numberOfElements).ToList(); 19 | 20 | //await ParallelAsync.ForEachAsync( 21 | var results = await ParallelAsync.ForEachAsync( 22 | collection: input, 23 | func: AddOne, 24 | maxBatchSize: batchSize, 25 | allowOutOfOrderProcessing: outOfOrder, 26 | estimatedResultSize: numberOfElements, 27 | cancellationToken: CancellationToken.None 28 | ) 29 | .ConfigureAwait(false); 30 | 31 | /*/ 32 | return 1; 33 | /*/ 34 | var resultCount = results.Count(); 35 | 36 | return (resultCount == numberOfElements) 37 | ? 0 38 | : numberOfElements - resultCount; 39 | //*/ 40 | } 41 | 42 | private static Task AddOne(int input) 43 | { 44 | return Task.FromResult(1 + input); 45 | } 46 | 47 | private static Task CompletedTask(int input) 48 | { 49 | return Task.CompletedTask; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/CSRakowski.Parallel.Benchmarks/Benchmarks/Computations.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using BenchmarkDotNet; 9 | using BenchmarkDotNet.Running; 10 | using BenchmarkDotNet.Reports; 11 | using BenchmarkDotNet.Attributes; 12 | using BenchmarkDotNet.Columns; 13 | using BenchmarkDotNet.Configs; 14 | using BenchmarkDotNet.Jobs; 15 | 16 | namespace CSRakowski.Parallel.Benchmarks 17 | { 18 | [MemoryDiagnoser] 19 | [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] 20 | [CategoriesColumn] 21 | #if OS_WINDOWS 22 | [SimpleJob(RuntimeMoniker.Net48, baseline: false)] 23 | #endif 24 | [SimpleJob(RuntimeMoniker.NetCoreApp31, baseline: false)] 25 | [SimpleJob(RuntimeMoniker.Net50, baseline: false)] 26 | [SimpleJob(RuntimeMoniker.Net60, baseline: true)] 27 | [SimpleJob(RuntimeMoniker.Net80, baseline: false)] 28 | public class Computations 29 | { 30 | private const int NumberOfItemsInCollection = 10000; 31 | 32 | private readonly List InputNumbers; 33 | 34 | public Computations() 35 | { 36 | InputNumbers = Enumerable.Range(0, NumberOfItemsInCollection).ToList(); 37 | } 38 | 39 | [Params(1, 4, 8)] 40 | public int MaxBatchSize { get; set; } 41 | 42 | [Params(false, true)] 43 | public bool AllowOutOfOrder { get; set; } 44 | 45 | [Benchmark, BenchmarkCategory("Compute_Double")] 46 | public Task Compute_Double() 47 | { 48 | return ParallelAsync.ForEachAsync(InputNumbers, TestFunctions.Compute_Double, MaxBatchSize, AllowOutOfOrder, NumberOfItemsInCollection, CancellationToken.None); 49 | } 50 | 51 | [Benchmark(Baseline = true), BenchmarkCategory("ReturnCompletedTask")] 52 | public Task ReturnCompletedTask() 53 | { 54 | return ParallelAsync.ForEachAsync(InputNumbers, TestFunctions.ReturnCompletedTask, MaxBatchSize, AllowOutOfOrder, CancellationToken.None); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/CSRakowski.Parallel.Benchmarks/Benchmarks/ParallelAsyncTestBenchmarks.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using BenchmarkDotNet; 9 | using BenchmarkDotNet.Running; 10 | using BenchmarkDotNet.Reports; 11 | using BenchmarkDotNet.Attributes; 12 | using BenchmarkDotNet.Columns; 13 | using BenchmarkDotNet.Configs; 14 | using BenchmarkDotNet.Jobs; 15 | 16 | namespace CSRakowski.Parallel.Benchmarks 17 | { 18 | [MemoryDiagnoser] 19 | [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByMethod, BenchmarkLogicalGroupRule.ByParams)] 20 | #if OS_WINDOWS 21 | [SimpleJob(RuntimeMoniker.Net48, baseline: false)] 22 | #endif 23 | [SimpleJob(RuntimeMoniker.NetCoreApp31, baseline: false)] 24 | [SimpleJob(RuntimeMoniker.Net50, baseline: false)] 25 | [SimpleJob(RuntimeMoniker.Net60, baseline: true)] 26 | [SimpleJob(RuntimeMoniker.Net80, baseline: false)] 27 | public class ParallelAsyncTestBenchmarks 28 | { 29 | private const int NumberOfItemsInCollection = 10000; 30 | 31 | private readonly List InputNumbers; 32 | 33 | public ParallelAsyncTestBenchmarks() 34 | { 35 | InputNumbers = Enumerable.Range(0, NumberOfItemsInCollection).ToList(); 36 | } 37 | 38 | [Params(4, 8)] 39 | public int MaxBatchSize { get; set; } 40 | 41 | [Params(false, true)] 42 | public bool AllowOutOfOrder { get; set; } 43 | 44 | [Benchmark, BenchmarkCategory("JustAddOne")] 45 | public Task JustAddOne() 46 | { 47 | return ParallelAsync.ForEachAsync(InputNumbers, TestFunctions.JustAddOne, MaxBatchSize, AllowOutOfOrder, NumberOfItemsInCollection, CancellationToken.None); 48 | } 49 | 50 | [Benchmark, BenchmarkCategory("ReturnTaskCompletedTask")] 51 | public Task ReturnTaskCompletedTask() 52 | { 53 | return ParallelAsync.ForEachAsync(InputNumbers, TestFunctions.ReturnCompletedTask, MaxBatchSize, AllowOutOfOrder, CancellationToken.None); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/CSRakowski.Parallel.Tests/HelpersTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using CSRakowski.Parallel; 8 | using Xunit; 9 | using CSRakowski.Parallel.Helpers; 10 | using System.Threading; 11 | using CSRakowski.Parallel.Tests.Helpers; 12 | 13 | namespace CSRakowski.Parallel.Tests 14 | { 15 | [Collection("ParallelAsync Helpers Tests")] 16 | public class HelpersTests 17 | { 18 | [Fact] 19 | public void ListHelper_Can_Determine_Sizes_Correctly() 20 | { 21 | var input = Enumerable.Range(1, 10).ToList(); 22 | 23 | IList list = input; 24 | ICollection collectionT = input; 25 | IReadOnlyCollection readOnlyCollection = new TestReadOnlyCollection(10); 26 | var collection = new TestCollection(10); 27 | 28 | IEnumerable enumerable = Enumerable.Range(1, 10); 29 | IEnumerable nullCollection = null; 30 | 31 | var listSize = ListHelpers.DetermineResultSize(list, -1); 32 | var readOnlyListSize = ListHelpers.DetermineResultSize(readOnlyCollection, -1); 33 | var collectionSize = ListHelpers.DetermineResultSize(collection, -1); 34 | var collectionTSize = ListHelpers.DetermineResultSize(collectionT, -1); 35 | 36 | var nullSize = ListHelpers.DetermineResultSize(nullCollection, -1); 37 | var enumerableSize = ListHelpers.DetermineResultSize(enumerable, -1); 38 | 39 | Assert.Equal(10, listSize); 40 | Assert.Equal(10, readOnlyListSize); 41 | Assert.Equal(10, collectionSize); 42 | Assert.Equal(10, collectionTSize); 43 | 44 | Assert.Equal(0, nullSize); 45 | 46 | #if NET8_0_OR_GREATER 47 | // Due to .NET internal refactorings around the RangeIterator, the ListHelper now picks this up as an ICollection, and we do actually get the actual size out of it. 48 | Assert.Equal(10, enumerableSize); 49 | #else 50 | Assert.Equal(-1, enumerableSize); 51 | #endif 52 | } 53 | 54 | [Fact] 55 | public void ListHelper_GetList_Handles_Negative_Numbers_Correctly() 56 | { 57 | var list1 = ListHelpers.GetList(10); 58 | var list2 = ListHelpers.GetList(0); 59 | var list3 = ListHelpers.GetList(-1); 60 | 61 | Assert.Equal(10, list1.Capacity); 62 | Assert.Equal(0, list2.Capacity); 63 | Assert.Equal(0, list3.Capacity); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/CSRakowski.Parallel.Benchmarks/Benchmarks/FuncOverloading.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using BenchmarkDotNet; 9 | using BenchmarkDotNet.Running; 10 | using BenchmarkDotNet.Reports; 11 | using BenchmarkDotNet.Attributes; 12 | using BenchmarkDotNet.Columns; 13 | using BenchmarkDotNet.Configs; 14 | using BenchmarkDotNet.Jobs; 15 | 16 | namespace CSRakowski.Parallel.Benchmarks 17 | { 18 | [MemoryDiagnoser] 19 | [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory, BenchmarkLogicalGroupRule.ByParams)] 20 | #if OS_WINDOWS 21 | [SimpleJob(RuntimeMoniker.Net48, baseline: false)] 22 | #endif 23 | [SimpleJob(RuntimeMoniker.NetCoreApp31, baseline: false)] 24 | [SimpleJob(RuntimeMoniker.Net50, baseline: false)] 25 | [SimpleJob(RuntimeMoniker.Net60, baseline: true)] 26 | [SimpleJob(RuntimeMoniker.Net80, baseline: false)] 27 | public class FuncOverloading 28 | { 29 | private const int NumberOfItemsInCollection = 10000; 30 | 31 | private readonly List InputNumbers; 32 | 33 | public FuncOverloading() 34 | { 35 | InputNumbers = Enumerable.Range(0, NumberOfItemsInCollection).ToList(); 36 | } 37 | 38 | [Params(1, 8)] 39 | public int MaxBatchSize { get; set; } 40 | 41 | [Params(false, true)] 42 | public bool AllowOutOfOrder { get; set; } 43 | 44 | [Benchmark, BenchmarkCategory("JustAddOne")] 45 | public Task JustAddOne_Wrapped() 46 | { 47 | return ParallelAsync.ForEachAsync(InputNumbers, TestFunctions.JustAddOne, MaxBatchSize, AllowOutOfOrder, NumberOfItemsInCollection, CancellationToken.None); 48 | } 49 | 50 | [Benchmark(Baseline = true), BenchmarkCategory("JustAddOne")] 51 | public Task JustAddOne_NotWrapped() 52 | { 53 | return ParallelAsync.ForEachAsync(InputNumbers, TestFunctions.JustAddOne_WithCancellationToken, MaxBatchSize, AllowOutOfOrder, NumberOfItemsInCollection, CancellationToken.None); 54 | } 55 | 56 | [Benchmark, BenchmarkCategory("CompletedTask")] 57 | public Task CompletedTask_Wrapped() 58 | { 59 | return ParallelAsync.ForEachAsync(InputNumbers, TestFunctions.ReturnCompletedTask, MaxBatchSize, AllowOutOfOrder, CancellationToken.None); 60 | } 61 | 62 | [Benchmark(Baseline = true), BenchmarkCategory("CompletedTask")] 63 | public Task CompletedTask_NotWrapped() 64 | { 65 | return ParallelAsync.ForEachAsync(InputNumbers, TestFunctions.ReturnCompletedTask_WithCancellationToken, MaxBatchSize, AllowOutOfOrder, CancellationToken.None); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/CSRakowski.Parallel.Benchmarks/Benchmarks/ParallelAsyncBenchmarks.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using BenchmarkDotNet; 9 | using BenchmarkDotNet.Running; 10 | using BenchmarkDotNet.Reports; 11 | using BenchmarkDotNet.Attributes; 12 | using BenchmarkDotNet.Columns; 13 | using BenchmarkDotNet.Configs; 14 | using BenchmarkDotNet.Jobs; 15 | 16 | namespace CSRakowski.Parallel.Benchmarks 17 | { 18 | [MemoryDiagnoser] 19 | [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] 20 | [CategoriesColumn] 21 | #if OS_WINDOWS 22 | [SimpleJob(RuntimeMoniker.Net48, baseline: false)] 23 | #endif 24 | [SimpleJob(RuntimeMoniker.NetCoreApp31, baseline: false)] 25 | [SimpleJob(RuntimeMoniker.Net50, baseline: false)] 26 | [SimpleJob(RuntimeMoniker.Net60, baseline: true)] 27 | [SimpleJob(RuntimeMoniker.Net80, baseline: false)] 28 | public class ParallelAsyncBenchmarks 29 | { 30 | private const int NumberOfItemsInCollection = 10000; 31 | 32 | private readonly List InputNumbers; 33 | 34 | public ParallelAsyncBenchmarks() 35 | { 36 | InputNumbers = Enumerable.Range(0, NumberOfItemsInCollection).ToList(); 37 | } 38 | 39 | [Params(1, 4, 8)] 40 | public int MaxBatchSize { get; set; } 41 | 42 | [Params(false, true)] 43 | public bool AllowOutOfOrder { get; set; } 44 | 45 | [Benchmark, BenchmarkCategory("JustAddOne", "QuickRun")] 46 | public Task JustAddOne() 47 | { 48 | return ParallelAsync.ForEachAsync(InputNumbers, TestFunctions.JustAddOne, MaxBatchSize, AllowOutOfOrder, NumberOfItemsInCollection, CancellationToken.None); 49 | } 50 | 51 | [Benchmark(Baseline = true), BenchmarkCategory("JustAddOne", "QuickRun")] 52 | public Task JustAddOne_WithCancellation() 53 | { 54 | return ParallelAsync.ForEachAsync(InputNumbers, TestFunctions.JustAddOne_WithCancellationToken, MaxBatchSize, AllowOutOfOrder, NumberOfItemsInCollection, CancellationToken.None); 55 | } 56 | 57 | [Benchmark, BenchmarkCategory("ReturnTaskCompletedTask", "QuickRun")] 58 | public Task ReturnTaskCompletedTask() 59 | { 60 | return ParallelAsync.ForEachAsync(InputNumbers, TestFunctions.ReturnCompletedTask, MaxBatchSize, AllowOutOfOrder, CancellationToken.None); 61 | } 62 | 63 | [Benchmark(Baseline = true), BenchmarkCategory("ReturnTaskCompletedTask", "QuickRun")] 64 | public Task ReturnTaskCompletedTask_WithCancellation() 65 | { 66 | return ParallelAsync.ForEachAsync(InputNumbers, TestFunctions.ReturnCompletedTask_WithCancellationToken, MaxBatchSize, AllowOutOfOrder, CancellationToken.None); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/CSRakowski.Parallel.Benchmarks/Benchmarks/ParallelAsyncBenchmarks_IAsyncEnumerable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using BenchmarkDotNet; 9 | using BenchmarkDotNet.Running; 10 | using BenchmarkDotNet.Reports; 11 | using BenchmarkDotNet.Attributes; 12 | using BenchmarkDotNet.Columns; 13 | using BenchmarkDotNet.Configs; 14 | using BenchmarkDotNet.Jobs; 15 | using CSRakowski.Parallel.Helpers; 16 | using CSRakowski.AsyncStreamsPreparations; 17 | 18 | namespace CSRakowski.Parallel.Benchmarks 19 | { 20 | [MemoryDiagnoser] 21 | [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] 22 | [CategoriesColumn] 23 | #if OS_WINDOWS 24 | [SimpleJob(RuntimeMoniker.Net48, baseline: false)] 25 | #endif 26 | [SimpleJob(RuntimeMoniker.NetCoreApp31, baseline: false)] 27 | [SimpleJob(RuntimeMoniker.Net50, baseline: false)] 28 | [SimpleJob(RuntimeMoniker.Net60, baseline: true)] 29 | [SimpleJob(RuntimeMoniker.Net80, baseline: false)] 30 | public class ParallelAsyncBenchmarks_IAsyncEnumerable 31 | { 32 | private const int NumberOfItemsInCollection = 10000; 33 | 34 | private readonly IAsyncEnumerable InputNumbers; 35 | 36 | public ParallelAsyncBenchmarks_IAsyncEnumerable() 37 | { 38 | InputNumbers = Enumerable.Range(0, NumberOfItemsInCollection).ToList().AsAsyncEnumerable(); 39 | } 40 | 41 | [Params(1, 4, 8)] 42 | public int MaxBatchSize { get; set; } 43 | 44 | [Params(false, true)] 45 | public bool AllowOutOfOrder { get; set; } 46 | 47 | [Benchmark, BenchmarkCategory("JustAddOne", "QuickRun")] 48 | public Task JustAddOne() 49 | { 50 | return ParallelAsync.ForEachAsync(InputNumbers, TestFunctions.JustAddOne, MaxBatchSize, AllowOutOfOrder, NumberOfItemsInCollection, CancellationToken.None); 51 | } 52 | 53 | [Benchmark(Baseline = true), BenchmarkCategory("JustAddOne", "QuickRun")] 54 | public Task JustAddOne_WithCancellation() 55 | { 56 | return ParallelAsync.ForEachAsync(InputNumbers, TestFunctions.JustAddOne_WithCancellationToken, MaxBatchSize, AllowOutOfOrder, NumberOfItemsInCollection, CancellationToken.None); 57 | } 58 | 59 | [Benchmark, BenchmarkCategory("ReturnTaskCompletedTask", "QuickRun")] 60 | public Task ReturnTaskCompletedTask() 61 | { 62 | return ParallelAsync.ForEachAsync(InputNumbers, TestFunctions.ReturnCompletedTask, MaxBatchSize, AllowOutOfOrder, CancellationToken.None); 63 | } 64 | 65 | [Benchmark(Baseline = true), BenchmarkCategory("ReturnTaskCompletedTask", "QuickRun")] 66 | public Task ReturnTaskCompletedTask_WithCancellation() 67 | { 68 | return ParallelAsync.ForEachAsync(InputNumbers, TestFunctions.ReturnCompletedTask_WithCancellationToken, MaxBatchSize, AllowOutOfOrder, CancellationToken.None); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/CSRakowski.Parallel/Helpers/ListHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace CSRakowski.Parallel.Helpers 7 | { 8 | /// 9 | /// A collection of helpers to get a of the right size 10 | /// 11 | /// 12 | /// These helpers are used by the class to get a big enough to hold the result collection 13 | /// 14 | public static class ListHelpers 15 | { 16 | /// 17 | /// Get's an empty list with enough capacity to hold the entire collection, or with the fallback value size. 18 | /// 19 | /// The type of the list elements 20 | /// The type of the 21 | /// The collection 22 | /// The fallback value 23 | /// The list 24 | public static List GetList(IEnumerable enumerable, int estimatedResultSize) 25 | { 26 | var size = DetermineResultSize(enumerable, estimatedResultSize); 27 | return GetList(size); 28 | } 29 | 30 | /// 31 | /// Attempt get the size of the , without actually consuming it. 32 | /// Falls back to if that is not possible. 33 | /// 34 | /// The type of the collection 35 | /// The collection 36 | /// The fallback value 37 | /// The size of the collection, or the fallback value 38 | public static int DetermineResultSize(IEnumerable enumerable, int estimatedResultSize) 39 | { 40 | switch (enumerable) 41 | { 42 | case null: 43 | return 0; 44 | case ICollection col: 45 | return col.Count; 46 | case ICollection col: 47 | return col.Count; 48 | case IReadOnlyCollection col: 49 | return col.Count; 50 | default: 51 | return estimatedResultSize; 52 | } 53 | } 54 | 55 | /// 56 | /// Get's an empty list with the specified capacity 57 | /// 58 | /// The type 59 | /// The capacity for the list 60 | /// The list 61 | /// 62 | /// Basically just calls the constructor overload with the specified capacity 63 | /// 64 | public static List GetList(int capacity) 65 | { 66 | return (capacity > 0) 67 | ? new List(capacity) 68 | : new List(); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/Profiling/Profiling.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {695E8128-39A1-4C05-B182-E6FD18786B0F} 8 | Exe 9 | Profiling 10 | Profiling 11 | v4.8 12 | 512 13 | true 14 | 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | false 26 | 27 | 28 | AnyCPU 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | false 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {a7eae71c-5122-41d5-81ce-8cd626f861af} 57 | CSRakowski.Parallel 58 | 59 | 60 | 61 | 62 | 1.6.0 63 | 64 | 65 | 7.0.0 66 | 67 | 68 | 6.1.2 69 | 70 | 71 | 4.6.3 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/CSRakowski.Parallel/Helpers/ParallelAsyncEventSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.Tracing; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace CSRakowski.Parallel.Helpers 9 | { 10 | /// 11 | /// The for 12 | /// 13 | [EventSource(Name = nameof(ParallelAsync))] 14 | internal sealed class ParallelAsyncEventSource : EventSource 15 | { 16 | /// 17 | /// The instance used for logging 18 | /// 19 | public static readonly ParallelAsyncEventSource Log = new ParallelAsyncEventSource(); 20 | 21 | #pragma warning disable CA1822 // Mark members as static 22 | 23 | /// 24 | /// Get's a unique number to use as the RunId 25 | /// 26 | /// A unique number to be used as the RunId 27 | public long GetRunId() => DateTime.UtcNow.Ticks; 28 | 29 | #pragma warning restore CA1822 // Mark members as static 30 | 31 | /// 32 | /// Writes a RunStart event 33 | /// 34 | /// The id of the current run 35 | /// The value of maxBatchSize for the run 36 | /// The value of allowOutOfOrderProcessing for the run 37 | /// The value of estimatedResultSize for the run 38 | [Event(1, Message = "Starting a new run (id: {0})", Opcode = EventOpcode.Start, Level = EventLevel.Informational)] 39 | public void RunStart(long runId, int maxBatchSize, bool allowOutOfOrderProcessing, int estimatedResultSize) 40 | { 41 | WriteEvent(1, runId, maxBatchSize, allowOutOfOrderProcessing, estimatedResultSize); 42 | } 43 | 44 | /// 45 | /// Writes a RunStop event 46 | /// 47 | /// The id of the current run 48 | [Event(2, Message = "Completed run (id: {0})", Opcode = EventOpcode.Stop, Level = EventLevel.Informational)] 49 | public void RunStop(long runId) 50 | { 51 | WriteEvent(2, runId); 52 | } 53 | 54 | /// 55 | /// Writes a BatchStart event 56 | /// 57 | /// The id of the current run 58 | /// The id of the current batch 59 | /// The size of the current batch 60 | [Event(3, Message = "Starting a new batch (runId: {0}, batchId: {1})", Opcode = EventOpcode.Start, Level = EventLevel.Informational)] 61 | public void BatchStart(long runId, int batchId, int batchSize) 62 | { 63 | WriteEvent(3, runId, batchId, batchSize); 64 | } 65 | 66 | /// 67 | /// Writes a BatchStop event 68 | /// 69 | /// The id of the current run 70 | /// The id of the current batch 71 | [Event(4, Message = "Completed batch (runId: {0}, batchId: {1})", Opcode = EventOpcode.Stop, Level = EventLevel.Informational)] 72 | public void BatchStop(long runId, int batchId) 73 | { 74 | WriteEvent(4, runId, batchId); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/CSRakowski.Parallel.Benchmarks/Benchmarks/ParallelAsyncBenchmarks_AsyncStreams.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using BenchmarkDotNet; 9 | using BenchmarkDotNet.Running; 10 | using BenchmarkDotNet.Reports; 11 | using BenchmarkDotNet.Attributes; 12 | using BenchmarkDotNet.Columns; 13 | using BenchmarkDotNet.Configs; 14 | using BenchmarkDotNet.Jobs; 15 | using CSRakowski.Parallel.Helpers; 16 | using CSRakowski.AsyncStreamsPreparations; 17 | 18 | namespace CSRakowski.Parallel.Benchmarks 19 | { 20 | [MemoryDiagnoser] 21 | [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] 22 | [CategoriesColumn] 23 | #if OS_WINDOWS 24 | [SimpleJob(RuntimeMoniker.Net48, baseline: false)] 25 | #endif 26 | [SimpleJob(RuntimeMoniker.NetCoreApp31, baseline: false)] 27 | [SimpleJob(RuntimeMoniker.Net50, baseline: false)] 28 | [SimpleJob(RuntimeMoniker.Net60, baseline: true)] 29 | [SimpleJob(RuntimeMoniker.Net80, baseline: false)] 30 | public class ParallelAsyncBenchmarks_AsyncStreams 31 | { 32 | private const int NumberOfItemsInCollection = 10000; 33 | 34 | private readonly IEnumerable InputNumbers; 35 | private readonly IAsyncEnumerable InputNumbersAsync; 36 | 37 | public ParallelAsyncBenchmarks_AsyncStreams() 38 | { 39 | InputNumbers = Enumerable.Range(0, NumberOfItemsInCollection).ToList(); 40 | InputNumbersAsync = InputNumbers.AsAsyncEnumerable(); 41 | } 42 | 43 | [Params(1, 4, 8)] 44 | public int MaxBatchSize { get; set; } 45 | 46 | [Params(false, true)] 47 | public bool AllowOutOfOrder { get; set; } 48 | 49 | [Benchmark(Baseline = true), BenchmarkCategory("ForEachAsync", "IEnumerable")] 50 | public async Task IEnumerable_ForEachAsync() 51 | { 52 | var result = await ParallelAsync.ForEachAsync(InputNumbers, TestFunctions.JustAddOne_WithCancellationToken, MaxBatchSize, AllowOutOfOrder, NumberOfItemsInCollection, CancellationToken.None); 53 | return result.Count(); 54 | } 55 | 56 | [Benchmark, BenchmarkCategory("ForEachAsync", "IAsyncEnumerable")] 57 | public async Task IAsyncEnumerable_ForEachAsync() 58 | { 59 | var result = await ParallelAsync.ForEachAsync(InputNumbersAsync, TestFunctions.JustAddOne_WithCancellationToken, MaxBatchSize, AllowOutOfOrder, NumberOfItemsInCollection, CancellationToken.None); 60 | return result.Count(); 61 | } 62 | 63 | [Benchmark(Baseline = true), BenchmarkCategory("ForEachAsyncStream", "IEnumerable")] 64 | public async Task IEnumerable_ForEachAsyncStream() 65 | { 66 | int count = 0; 67 | 68 | await foreach (var r in ParallelAsync.ForEachAsyncStream(InputNumbers, TestFunctions.JustAddOne_WithCancellationToken, MaxBatchSize, AllowOutOfOrder, NumberOfItemsInCollection, CancellationToken.None)) 69 | { 70 | count++; 71 | } 72 | 73 | return count; 74 | } 75 | 76 | [Benchmark, BenchmarkCategory("ForEachAsyncStream", "IAsyncEnumerable")] 77 | public async Task IAsyncEnumerable_ForEachAsyncStream() 78 | { 79 | int count = 0; 80 | 81 | await foreach (var r in ParallelAsync.ForEachAsyncStream(InputNumbersAsync, TestFunctions.JustAddOne_WithCancellationToken, MaxBatchSize, AllowOutOfOrder, NumberOfItemsInCollection, CancellationToken.None)) 82 | { 83 | count++; 84 | } 85 | 86 | return count; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /.github/workflows/dotnetcore.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: Build and test 8 | 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | os: [windows-latest, ubuntu-latest] 14 | framework: ['net60', 'net80', 'net90'] 15 | steps: 16 | 17 | - name: Check out code onto host 18 | uses: actions/checkout@v6 19 | 20 | - name: Setup .Net 6.0 21 | uses: actions/setup-dotnet@v5 22 | with: 23 | dotnet-version: '6.0.x' # SDK Version to use. 24 | 25 | - name: Setup .Net 8.0 26 | uses: actions/setup-dotnet@v5 27 | with: 28 | dotnet-version: '8.0.x' # SDK Version to use. 29 | 30 | - name: Setup .Net 9.0 31 | uses: actions/setup-dotnet@v5 32 | with: 33 | dotnet-version: '9.0.x' # SDK Version to use. 34 | 35 | - name: Dotnet info 36 | run: | 37 | dotnet --version 38 | dotnet --info 39 | 40 | - name: Clear nuget cache 41 | run: | 42 | dotnet clean 43 | dotnet nuget locals all --clear 44 | 45 | - name: Dotnet restore 46 | run: | 47 | dotnet restore 48 | 49 | - name: Build and Run unit tests 50 | continue-on-error: true 51 | run: | 52 | dotnet test --no-restore --configuration Release --verbosity normal --framework=${{ matrix.framework }} --logger trx --results-directory "TestResults-${{ matrix.os }}-${{ matrix.framework }}" 53 | 54 | #Benchmarks: 55 | - name: Run Benchmarks 56 | run: | 57 | dotnet run --no-restore --configuration Release --verbosity normal --framework=${{ matrix.framework }} --project ./tests/CSRakowski.Parallel.Benchmarks/ 58 | if: matrix.framework == 'net80' 59 | 60 | - name: Upload dotnet test results 61 | uses: actions/upload-artifact@v5 62 | with: 63 | name: dotnet-results-${{ matrix.os }}-${{ matrix.framework }} 64 | path: TestResults-${{ matrix.os }}-${{ matrix.framework }} 65 | # Use always() to always run this step to publish test results when there are test failures 66 | if: ${{ always() }} 67 | 68 | - name: Upload BenchmarkDotNet results 69 | uses: actions/upload-artifact@v5 70 | with: 71 | name: BenchmarkDotNet-${{ matrix.os }}-${{ matrix.framework }} 72 | path: BenchmarkDotNet.Artifacts 73 | # Use always() to always run this step to publish test results when there are test failures 74 | if: ${{ always() }} 75 | 76 | build-netfx: 77 | name: Build and test .NET Framework 78 | 79 | runs-on: windows-latest 80 | strategy: 81 | fail-fast: false 82 | matrix: 83 | framework: ['net472', 'net48'] 84 | steps: 85 | 86 | - name: Check out code onto host 87 | uses: actions/checkout@v6 88 | 89 | - name: Add msbuild to PATH 90 | uses: microsoft/setup-msbuild@v2 91 | 92 | - name: Setup .Net 8.0 93 | uses: actions/setup-dotnet@v5 94 | with: 95 | dotnet-version: '8.0.x' # SDK Version to use. 96 | 97 | - name: Dotnet info 98 | run: | 99 | dotnet --version 100 | dotnet --info 101 | 102 | - name: Clear nuget cache 103 | run: | 104 | dotnet clean 105 | dotnet nuget locals all --clear 106 | 107 | - name: Dotnet restore 108 | run: | 109 | dotnet restore 110 | 111 | - name: Build and Run unit tests 112 | continue-on-error: true 113 | run: | 114 | dotnet test --no-restore --configuration Release --verbosity normal --framework=${{ matrix.framework }} --logger trx --results-directory "TestResults-${{ matrix.os }}-${{ matrix.framework }}" 115 | 116 | - name: Upload dotnet test results 117 | uses: actions/upload-artifact@v5 118 | with: 119 | name: dotnet-results-windows-latest-${{ matrix.framework }} 120 | path: TestResults-windows-latest-${{ matrix.framework }} 121 | # Use always() to always run this step to publish test results when there are test failures 122 | if: ${{ always() }} 123 | -------------------------------------------------------------------------------- /src/CSRakowski.Parallel/Extensions/ParallelAsyncEx.AsyncStreams.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.CompilerServices; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace CSRakowski.Parallel.Extensions 10 | { 11 | /// 12 | /// Extension methods to allow using the functionalities of with a fluent syntax 13 | /// 14 | public static partial class ParallelAsyncEx 15 | { 16 | #region ForEachAsyncStream overloads 17 | 18 | /// 19 | /// Runs the specified async method for each item of the input collection in a parallel/batched manner. 20 | /// 21 | /// The input item type 22 | /// The result item type 23 | /// The to process 24 | /// The async method to run for each item 25 | /// A 26 | /// The results of the operations 27 | /// Thrown when either or is null. 28 | /// Thrown when the configured maximum batch size is a negative number. 29 | public static IAsyncEnumerable ForEachAsyncStream(this IParallelAsyncEnumerable parallelAsync, Func> func, CancellationToken cancellationToken = default) 30 | { 31 | var obj = EnsureValidEnumerable(parallelAsync); 32 | 33 | if (obj.IsAsyncEnumerable) 34 | { 35 | return ParallelAsync.ForEachAsyncStream(obj.AsyncEnumerable, func, obj.MaxDegreeOfParallelism, obj.AllowOutOfOrderProcessing, obj.EstimatedResultSize, cancellationToken); 36 | } 37 | else 38 | { 39 | return ParallelAsync.ForEachAsyncStream(obj.Enumerable, func, obj.MaxDegreeOfParallelism, obj.AllowOutOfOrderProcessing, obj.EstimatedResultSize, cancellationToken); 40 | } 41 | } 42 | 43 | /// 44 | /// Runs the specified async method for each item of the input collection in a parallel/batched manner. 45 | /// 46 | /// The input item type 47 | /// The result item type 48 | /// The to process 49 | /// The async method to run for each item 50 | /// A 51 | /// The results of the operations 52 | /// Thrown when either or is null. 53 | /// Thrown when the configured maximum batch size is a negative number. 54 | public static IAsyncEnumerable ForEachAsyncStream(this IParallelAsyncEnumerable parallelAsync, Func> func, CancellationToken cancellationToken = default) 55 | { 56 | var obj = EnsureValidEnumerable(parallelAsync); 57 | 58 | if (obj.IsAsyncEnumerable) 59 | { 60 | return ParallelAsync.ForEachAsyncStream(obj.AsyncEnumerable, func, obj.MaxDegreeOfParallelism, obj.AllowOutOfOrderProcessing, obj.EstimatedResultSize, cancellationToken); 61 | } 62 | else 63 | { 64 | return ParallelAsync.ForEachAsyncStream(obj.Enumerable, func, obj.MaxDegreeOfParallelism, obj.AllowOutOfOrderProcessing, obj.EstimatedResultSize, cancellationToken); 65 | } 66 | } 67 | 68 | #endregion ForEachAsyncStream overloads 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ParallelAsync 2 | A .NET utility library for running async methods in parallel batches. 3 | 4 | Available on NuGet: [![NuGet](https://img.shields.io/nuget/v/CSRakowski.ParallelAsync.svg)](https://www.nuget.org/packages/CSRakowski.ParallelAsync/) 5 | and GitHub: [![GitHub stars](https://img.shields.io/github/stars/csrakowski/ParallelAsync.svg)](https://github.com/csrakowski/ParallelAsync/) 6 | 7 | On the side, working on improving usage of [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8154/badge)](https://www.bestpractices.dev/projects/8154) 8 | 9 | Example usage: 10 | ```cs 11 | using CSRakowski.Parallel; 12 | 13 | List fileUrls = GetFileUrls(); 14 | 15 | var files = await ParallelAsync.ForEachAsync(fileUrls, (url) => { 16 | return DownloadFileAsync(url); 17 | }, maxBatchSize: 8, allowOutOfOrderProcessing: true); 18 | ``` 19 | 20 | As of version 1.1 a fluent syntax is also available: 21 | ```cs 22 | using CSRakowski.Parallel.Extensions; 23 | 24 | List fileUrls = GetFileUrls(); 25 | 26 | var files = await fileUrls 27 | .AsParallelAsync() 28 | .WithMaxDegreeOfParallelism(8) 29 | .WithOutOfOrderProcessing(false) 30 | .ForEachAsync((url) => { 31 | return DownloadFileAsync(url); 32 | }); 33 | ``` 34 | 35 | In version 1.6, support for Async Streams has been added. 36 | This allows you to chain together multiple invocations by passing along an `IAsyncEnumerable`, sort of like a pipeline: 37 | 38 | ```cs 39 | using CSRakowski.Parallel; 40 | 41 | List fileUrls = GetFileUrls(); 42 | 43 | var fileDataStream = ParallelAsync.ForEachAsyncStream(fileUrls, (url) => { 44 | return DownloadFileAsync(url); 45 | }, maxBatchSize: 4, allowOutOfOrderProcessing: true); 46 | 47 | var resultStream = ParallelAsync.ForEachAsyncStream(fileDataStream, (fileData) => { 48 | return ParseFileAsync(fileData); 49 | }, maxBatchSize: 4, allowOutOfOrderProcessing: true); 50 | 51 | await foreach (var result in resultStream) 52 | { 53 | HandleResult(result); 54 | } 55 | ``` 56 | 57 | 58 | # Release notes 59 | 60 | ### 1.8.0 61 | * Updated TargetFrameworks to remove old unsupported ones. 62 | 63 | ### 1.7.2 64 | * First attempt at enabling SourceLink. 65 | 66 | ### 1.7.1 67 | * Updated to latest `CSRakowski.AsyncStreamsPreparations`, which uses `Microsoft.Bcl.AsyncInterfaces` (correctly...). 68 | 69 | ### 1.7.0 70 | * Updated to latest `CSRakowski.AsyncStreamsPreparations`, which uses `Microsoft.Bcl.AsyncInterfaces`. 71 | 72 | ### 1.6.0 73 | * Added support for Async Streams, so you produce an `IAsyncEnumerable`. 74 | 75 | ### 1.5.4 76 | * Added .NET 6.0 TargetFramework 77 | 78 | ### 1.5.2 79 | * Fixed dependency misconfiguration on net50 80 | 81 | ### 1.5.1 82 | * Updated target frameworks 83 | 84 | ### 1.5.0 85 | * Updated target frameworks 86 | 87 | ### 1.4.1 88 | * Updated dependencies 89 | 90 | ### 1.4 91 | * Added gist support for `IAsyncEnumberable` 92 | 93 | ### 1.3.2 94 | * Added the RunId to the BatchStart and BatchStop events 95 | 96 | ### 1.3.1 97 | * Reduced overhead in code paths where the input collection is a `T[]`, `maxBatchSize` is greater than `1` and `allowOutOfOrder` is `false` 98 | 99 | ### 1.3 100 | * Changed assembly signing key 101 | * Further changes to internal implementation details 102 | * Performance improvements when the input collection is a `T[]` or `IList` and `maxBatchSize` is set to `1` 103 | * Performance improvements in the `allowOutOfOrder` code paths. 104 | 105 | ### 1.2.1 106 | * Marked the `T` on the `IParallelAsyncEnumerable` as covariant 107 | * Changes to internal implementation details 108 | 109 | ### 1.2 110 | * Added an `EventSource` to expose some diagnostic information. 111 | * Changed minimum supported NetStandard from 1.0 to 1.1 (Because of the `EventSource`). 112 | 113 | ### 1.1.1 114 | * Added support for `IReadOnlyCollection` to the `ListHelper`. 115 | * Added more XmlDoc to methods and classes. 116 | 117 | ### 1.1 118 | * Renamed class to `ParallelAsync` to prevent naming conflicts with the `System.Threading.Tasks.Parallel`. 119 | * Renamed namespace to `CSRakowski.Parallel` to prevent ambiguous name conflicts between the class and the namespace. 120 | * Added new extension methods to allow for fluent sytax usage. 121 | 122 | ### 1.0.1 123 | * Enabled Strong Naming. 124 | 125 | ### 1.0 126 | * Initial release. 127 | -------------------------------------------------------------------------------- /src/CSRakowski.Parallel/Extensions/ParallelAsyncEnumerable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | 5 | namespace CSRakowski.Parallel.Extensions 6 | { 7 | /// 8 | /// Internal helper class that wraps an or , and configuration values used by 9 | /// 10 | /// The element type 11 | internal class ParallelAsyncEnumerable : IParallelAsyncEnumerable 12 | { 13 | /// 14 | /// The wrapped 15 | /// 16 | /// 17 | /// Will be null if is true 18 | /// 19 | internal readonly IEnumerable Enumerable; 20 | 21 | /// 22 | /// The wrapped 23 | /// 24 | /// 25 | /// Will be null if is false 26 | /// 27 | internal readonly IAsyncEnumerable AsyncEnumerable; 28 | 29 | /// 30 | /// Indicates that the wrapped collection is an 31 | /// 32 | internal bool IsAsyncEnumerable => AsyncEnumerable != null; 33 | 34 | /// 35 | /// The maximum batch size to allow 36 | /// 37 | internal int MaxDegreeOfParallelism { get; set; } 38 | 39 | /// 40 | /// The estimated result size 41 | /// 42 | internal int EstimatedResultSize { get; set; } 43 | 44 | /// 45 | /// Whether or not to allow out of order processing 46 | /// 47 | internal bool AllowOutOfOrderProcessing { get; set; } 48 | 49 | /// 50 | /// Instantiates a new that wraps the specified 51 | /// 52 | /// The to wrap 53 | /// Thrown when is null 54 | internal ParallelAsyncEnumerable(IEnumerable enumerable) 55 | { 56 | Enumerable = enumerable ?? throw new ArgumentNullException(nameof(enumerable)); 57 | MaxDegreeOfParallelism = 0; 58 | EstimatedResultSize = 0; 59 | AllowOutOfOrderProcessing = false; 60 | } 61 | 62 | /// 63 | /// Instantiates a new that wraps the specified 64 | /// 65 | /// The to wrap 66 | /// Thrown when is null 67 | internal ParallelAsyncEnumerable(IAsyncEnumerable enumerable) 68 | { 69 | AsyncEnumerable = enumerable ?? throw new ArgumentNullException(nameof(enumerable)); 70 | MaxDegreeOfParallelism = 0; 71 | EstimatedResultSize = 0; 72 | AllowOutOfOrderProcessing = false; 73 | } 74 | 75 | /// 76 | public IEnumerator GetEnumerator() 77 | { 78 | if (IsAsyncEnumerable) 79 | { 80 | if (AsyncEnumerable is IEnumerable enumerable) 81 | { 82 | return enumerable.GetEnumerator(); 83 | } 84 | else 85 | { 86 | throw new NotSupportedException(); 87 | } 88 | } 89 | 90 | return Enumerable.GetEnumerator(); 91 | } 92 | 93 | /// 94 | IEnumerator IEnumerable.GetEnumerator() 95 | { 96 | if (IsAsyncEnumerable) 97 | { 98 | if (AsyncEnumerable is IEnumerable enumerable) 99 | { 100 | return enumerable.GetEnumerator(); 101 | } 102 | else 103 | { 104 | throw new NotSupportedException(); 105 | } 106 | } 107 | 108 | return ((IEnumerable)Enumerable).GetEnumerator(); 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /tests/CSRakowski.Parallel.Tests/ExtensionMethodsTests_AsyncStreams.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using CSRakowski.Parallel; 8 | using Xunit; 9 | using CSRakowski.Parallel.Extensions; 10 | using System.Threading; 11 | using CSRakowski.Parallel.Helpers; 12 | using CSRakowski.Parallel.Tests.Helpers; 13 | using CSRakowski.AsyncStreamsPreparations; 14 | 15 | namespace CSRakowski.Parallel.Tests 16 | { 17 | [Collection("ParallelAsync AsyncStreams Extension Methods Tests")] 18 | public class ExtensionMethodsTests_AsyncStreams 19 | { 20 | [Fact] 21 | public async Task ParallelAsync_Runs_With_Default_Settings() 22 | { 23 | var input = Enumerable.Range(1, 10).ToList().AsAsyncEnumerable(); 24 | 25 | var parallelAsync = input.AsParallelAsync(); 26 | 27 | Assert.NotNull(parallelAsync); 28 | 29 | var list = new List(); 30 | 31 | await foreach (var item in parallelAsync.ForEachAsyncStream((el) => Task.FromResult(el * 2))) 32 | { 33 | list.Add(item); 34 | } 35 | 36 | Assert.Equal(10, list.Count); 37 | 38 | for (int i = 0; i < list.Count; i++) 39 | { 40 | var expected = 2 * (1 + i); 41 | Assert.Equal(expected, list[i]); 42 | } 43 | } 44 | 45 | [Fact] 46 | public async Task ParallelAsync_Runs_With_Default_Settings2() 47 | { 48 | var input = Enumerable.Range(1, 10).ToList().AsAsyncEnumerable(); 49 | 50 | var parallelAsync = input.AsParallelAsync(); 51 | 52 | Assert.NotNull(parallelAsync); 53 | 54 | var list = new List(); 55 | 56 | await foreach (var item in parallelAsync.ForEachAsyncStream((el, ct) => Task.FromResult(el * 2))) 57 | { 58 | list.Add(item); 59 | } 60 | 61 | Assert.Equal(10, list.Count); 62 | 63 | for (int i = 0; i < list.Count; i++) 64 | { 65 | var expected = 2 * (1 + i); 66 | Assert.Equal(expected, list[i]); 67 | } 68 | } 69 | 70 | [Fact] 71 | public async Task ParallelAsync_Supports_Full_Fluent_Usage() 72 | { 73 | var asyncEnumerable = Enumerable 74 | .Range(1, 10) 75 | .AsAsyncEnumerable() 76 | .AsParallelAsync() 77 | .WithEstimatedResultSize(10) 78 | .WithMaxDegreeOfParallelism(2) 79 | .WithOutOfOrderProcessing(false) 80 | .ForEachAsyncStream((el) => Task.FromResult(el * 2), CancellationToken.None); 81 | 82 | Assert.NotNull(asyncEnumerable); 83 | 84 | var list = new List(); 85 | 86 | await foreach (var item in asyncEnumerable) 87 | { 88 | list.Add(item); 89 | } 90 | 91 | Assert.Equal(10, list.Count); 92 | 93 | for (int i = 0; i < list.Count; i++) 94 | { 95 | var expected = 2 * (1 + i); 96 | Assert.Equal(expected, list[i]); 97 | } 98 | } 99 | 100 | [Fact] 101 | public async Task ParallelAsync_Can_Chain_Together_AsyncStreams() 102 | { 103 | var input = Enumerable.Range(1, 40).ToList().AsAsyncEnumerable(); 104 | 105 | var parallelAsync = input.AsParallelAsync(); 106 | 107 | Assert.NotNull(parallelAsync); 108 | 109 | var list = new List(); 110 | 111 | IAsyncEnumerable intermediateResult = parallelAsync.ForEachAsyncStream((el, ct) => Task.FromResult(el * 2)); 112 | 113 | var intermediateParallelAsync = intermediateResult.AsParallelAsync(); 114 | 115 | await foreach (var item in intermediateParallelAsync.ForEachAsyncStream((el, ct) => Task.FromResult(el * 2))) 116 | { 117 | list.Add(item); 118 | } 119 | 120 | Assert.Equal(40, list.Count); 121 | 122 | for (int i = 0; i < list.Count; i++) 123 | { 124 | var expected = 4 * (1 + i); 125 | Assert.Equal(expected, list[i]); 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/CSRakowski.Parallel.Benchmarks/Benchmarks/UsingForEachForUnbatched.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections.ObjectModel; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using BenchmarkDotNet; 11 | using BenchmarkDotNet.Running; 12 | using BenchmarkDotNet.Reports; 13 | using BenchmarkDotNet.Attributes; 14 | using BenchmarkDotNet.Columns; 15 | using BenchmarkDotNet.Configs; 16 | using BenchmarkDotNet.Jobs; 17 | 18 | namespace CSRakowski.Parallel.Benchmarks 19 | { 20 | [MemoryDiagnoser] 21 | [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] 22 | #if OS_WINDOWS 23 | [SimpleJob(RuntimeMoniker.Net48, baseline: false)] 24 | #endif 25 | [SimpleJob(RuntimeMoniker.NetCoreApp31, baseline: false)] 26 | [SimpleJob(RuntimeMoniker.Net50, baseline: false)] 27 | [SimpleJob(RuntimeMoniker.Net60, baseline: true)] 28 | [SimpleJob(RuntimeMoniker.Net80, baseline: false)] 29 | public class UsingForEachForUnbatched 30 | { 31 | private const int NumberOfItemsInCollection = 1000000; 32 | 33 | private readonly int[] InputNumbersArray; 34 | private readonly List InputNumbersList; 35 | private readonly ReadOnlyCollection InputNumbersReadOnlyList; 36 | private IEnumerable InputNumbersEnumerable { get { return Enumerable.Range(0, NumberOfItemsInCollection); } } 37 | 38 | public UsingForEachForUnbatched() 39 | { 40 | InputNumbersArray = Enumerable.Range(0, NumberOfItemsInCollection).ToArray(); 41 | InputNumbersList = InputNumbersArray.ToList(); 42 | InputNumbersReadOnlyList = InputNumbersList.AsReadOnly(); 43 | } 44 | 45 | public int MaxBatchSize { get; set; } = 1; 46 | 47 | public bool AllowOutOfOrder { get; set; } = false; 48 | 49 | 50 | // JustAddOne 51 | 52 | [Benchmark(Baseline = true), BenchmarkCategory("JustAddOne")] 53 | public Task JustAddOne_List() 54 | { 55 | return ParallelAsync.ForEachAsync(InputNumbersList, TestFunctions.JustAddOne, MaxBatchSize, AllowOutOfOrder, NumberOfItemsInCollection, CancellationToken.None); 56 | } 57 | 58 | [Benchmark, BenchmarkCategory("JustAddOne")] 59 | public Task JustAddOne_ReadOnlyList() 60 | { 61 | return ParallelAsync.ForEachAsync(InputNumbersReadOnlyList, TestFunctions.JustAddOne, MaxBatchSize, AllowOutOfOrder, NumberOfItemsInCollection, CancellationToken.None); 62 | } 63 | 64 | [Benchmark, BenchmarkCategory("JustAddOne")] 65 | public Task JustAddOne_Enumerable() 66 | { 67 | return ParallelAsync.ForEachAsync(InputNumbersEnumerable, TestFunctions.JustAddOne, MaxBatchSize, AllowOutOfOrder, NumberOfItemsInCollection, CancellationToken.None); 68 | } 69 | 70 | [Benchmark, BenchmarkCategory("JustAddOne")] 71 | public Task JustAddOne_Array() 72 | { 73 | return ParallelAsync.ForEachAsync(InputNumbersArray, TestFunctions.JustAddOne, MaxBatchSize, AllowOutOfOrder, NumberOfItemsInCollection, CancellationToken.None); 74 | } 75 | 76 | 77 | // ReturnTaskCompletedTask 78 | 79 | [Benchmark(Baseline = true), BenchmarkCategory("ReturnTaskCompletedTask")] 80 | public Task ReturnTaskCompletedTask_List() 81 | { 82 | return ParallelAsync.ForEachAsync(InputNumbersList, TestFunctions.ReturnCompletedTask, MaxBatchSize, AllowOutOfOrder, CancellationToken.None); 83 | } 84 | 85 | [Benchmark, BenchmarkCategory("ReturnTaskCompletedTask")] 86 | public Task ReturnTaskCompletedTask_ReadOnlyList() 87 | { 88 | return ParallelAsync.ForEachAsync(InputNumbersReadOnlyList, TestFunctions.ReturnCompletedTask, MaxBatchSize, AllowOutOfOrder, CancellationToken.None); 89 | } 90 | 91 | [Benchmark, BenchmarkCategory("ReturnTaskCompletedTask")] 92 | public Task ReturnTaskCompletedTask_Enumerable() 93 | { 94 | return ParallelAsync.ForEachAsync(InputNumbersEnumerable, TestFunctions.ReturnCompletedTask, MaxBatchSize, AllowOutOfOrder, CancellationToken.None); 95 | } 96 | 97 | [Benchmark, BenchmarkCategory("ReturnTaskCompletedTask")] 98 | public Task ReturnTaskCompletedTask_Array() 99 | { 100 | return ParallelAsync.ForEachAsync(InputNumbersArray, TestFunctions.ReturnCompletedTask, MaxBatchSize, AllowOutOfOrder, CancellationToken.None); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /CSRakowski.ParallelAsync.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32407.343 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSRakowski.Parallel", "src\CSRakowski.Parallel\CSRakowski.Parallel.csproj", "{A7EAE71C-5122-41D5-81CE-8CD626F861AF}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSRakowski.Parallel.Tests", "tests\CSRakowski.Parallel.Tests\CSRakowski.Parallel.Tests.csproj", "{8EDF4429-251A-416D-BB68-93F227191BCF}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{7A1BFA5A-BF85-4BF2-ADBB-D7DAD2464FAF}" 11 | ProjectSection(SolutionItems) = preProject 12 | .gitignore = .gitignore 13 | Directory.Build.props = Directory.Build.props 14 | global.json = global.json 15 | LICENSE = LICENSE 16 | README.md = README.md 17 | EndProjectSection 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2DA84356-AD18-4DC9-91AF-45C018372075}" 20 | EndProject 21 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{1C224AEE-DF65-40FE-86C6-47DFF972D702}" 22 | EndProject 23 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSRakowski.Parallel.Benchmarks", "tests\CSRakowski.Parallel.Benchmarks\CSRakowski.Parallel.Benchmarks.csproj", "{77A7B5F9-E2C0-428D-9AF0-390E8597CF52}" 24 | EndProject 25 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Profiling", "tests\Profiling\Profiling.csproj", "{695E8128-39A1-4C05-B182-E6FD18786B0F}" 26 | ProjectSection(ProjectDependencies) = postProject 27 | {A7EAE71C-5122-41D5-81CE-8CD626F861AF} = {A7EAE71C-5122-41D5-81CE-8CD626F861AF} 28 | EndProjectSection 29 | EndProject 30 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{72274E52-335B-45D7-A85A-194A4AC886AA}" 31 | ProjectSection(SolutionItems) = preProject 32 | .github\dependabot.yml = .github\dependabot.yml 33 | EndProjectSection 34 | EndProject 35 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{41D6F74A-4D0E-471E-864A-339A8CBAF6E7}" 36 | ProjectSection(SolutionItems) = preProject 37 | .github\workflows\dotnetcore.yml = .github\workflows\dotnetcore.yml 38 | EndProjectSection 39 | EndProject 40 | Global 41 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 42 | Debug|Any CPU = Debug|Any CPU 43 | Release|Any CPU = Release|Any CPU 44 | EndGlobalSection 45 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 46 | {A7EAE71C-5122-41D5-81CE-8CD626F861AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {A7EAE71C-5122-41D5-81CE-8CD626F861AF}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {A7EAE71C-5122-41D5-81CE-8CD626F861AF}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {A7EAE71C-5122-41D5-81CE-8CD626F861AF}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {8EDF4429-251A-416D-BB68-93F227191BCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {8EDF4429-251A-416D-BB68-93F227191BCF}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {8EDF4429-251A-416D-BB68-93F227191BCF}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {8EDF4429-251A-416D-BB68-93F227191BCF}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {77A7B5F9-E2C0-428D-9AF0-390E8597CF52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {77A7B5F9-E2C0-428D-9AF0-390E8597CF52}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {77A7B5F9-E2C0-428D-9AF0-390E8597CF52}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {77A7B5F9-E2C0-428D-9AF0-390E8597CF52}.Release|Any CPU.Build.0 = Release|Any CPU 58 | {695E8128-39A1-4C05-B182-E6FD18786B0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 59 | {695E8128-39A1-4C05-B182-E6FD18786B0F}.Debug|Any CPU.Build.0 = Debug|Any CPU 60 | {695E8128-39A1-4C05-B182-E6FD18786B0F}.Release|Any CPU.ActiveCfg = Release|Any CPU 61 | {695E8128-39A1-4C05-B182-E6FD18786B0F}.Release|Any CPU.Build.0 = Release|Any CPU 62 | EndGlobalSection 63 | GlobalSection(SolutionProperties) = preSolution 64 | HideSolutionNode = FALSE 65 | EndGlobalSection 66 | GlobalSection(NestedProjects) = preSolution 67 | {A7EAE71C-5122-41D5-81CE-8CD626F861AF} = {2DA84356-AD18-4DC9-91AF-45C018372075} 68 | {8EDF4429-251A-416D-BB68-93F227191BCF} = {1C224AEE-DF65-40FE-86C6-47DFF972D702} 69 | {77A7B5F9-E2C0-428D-9AF0-390E8597CF52} = {1C224AEE-DF65-40FE-86C6-47DFF972D702} 70 | {695E8128-39A1-4C05-B182-E6FD18786B0F} = {1C224AEE-DF65-40FE-86C6-47DFF972D702} 71 | {72274E52-335B-45D7-A85A-194A4AC886AA} = {7A1BFA5A-BF85-4BF2-ADBB-D7DAD2464FAF} 72 | {41D6F74A-4D0E-471E-864A-339A8CBAF6E7} = {72274E52-335B-45D7-A85A-194A4AC886AA} 73 | EndGlobalSection 74 | GlobalSection(ExtensibilityGlobals) = postSolution 75 | SolutionGuid = {1D0D0442-0469-4C2F-A36A-16F825704000} 76 | EndGlobalSection 77 | EndGlobal 78 | -------------------------------------------------------------------------------- /tests/CSRakowski.Parallel.Benchmarks/Benchmarks/UsingForEachForOrdered.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections.ObjectModel; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using BenchmarkDotNet; 11 | using BenchmarkDotNet.Running; 12 | using BenchmarkDotNet.Reports; 13 | using BenchmarkDotNet.Attributes; 14 | using BenchmarkDotNet.Columns; 15 | using BenchmarkDotNet.Configs; 16 | using BenchmarkDotNet.Jobs; 17 | 18 | namespace CSRakowski.Parallel.Benchmarks 19 | { 20 | [MemoryDiagnoser] 21 | [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] 22 | #if OS_WINDOWS 23 | [SimpleJob(RuntimeMoniker.Net48, baseline: false)] 24 | #endif 25 | [SimpleJob(RuntimeMoniker.NetCoreApp31, baseline: false)] 26 | [SimpleJob(RuntimeMoniker.Net50, baseline: false)] 27 | [SimpleJob(RuntimeMoniker.Net60, baseline: true)] 28 | [SimpleJob(RuntimeMoniker.Net80, baseline: false)] 29 | public class UsingForEachForOrdered 30 | { 31 | private const int NumberOfItemsInCollection = 100000; 32 | 33 | private readonly int[] InputNumbersArray; 34 | private readonly List InputNumbersList; 35 | private readonly ReadOnlyCollection InputNumbersReadOnlyList; 36 | private IEnumerable InputNumbersEnumerable { get { return Enumerable.Range(0, NumberOfItemsInCollection); } } 37 | 38 | public UsingForEachForOrdered() 39 | { 40 | InputNumbersArray = Enumerable.Range(0, NumberOfItemsInCollection).ToArray(); 41 | InputNumbersList = InputNumbersArray.ToList(); 42 | InputNumbersReadOnlyList = InputNumbersList.AsReadOnly(); 43 | } 44 | 45 | public int MaxBatchSize { get; set; } = 8; 46 | 47 | public bool AllowOutOfOrder { get; set; } = false; 48 | 49 | // JustAddOne 50 | 51 | [Benchmark(Baseline = true), BenchmarkCategory("JustAddOne")] 52 | public Task JustAddOne_List() 53 | { 54 | return ParallelAsync.ForEachAsync(InputNumbersList, TestFunctions.JustAddOne_WithCancellationToken, MaxBatchSize, AllowOutOfOrder, NumberOfItemsInCollection, CancellationToken.None); 55 | } 56 | 57 | [Benchmark, BenchmarkCategory("JustAddOne")] 58 | public Task JustAddOne_ReadOnlyList() 59 | { 60 | return ParallelAsync.ForEachAsync(InputNumbersReadOnlyList, TestFunctions.JustAddOne_WithCancellationToken, MaxBatchSize, AllowOutOfOrder, NumberOfItemsInCollection, CancellationToken.None); 61 | } 62 | 63 | [Benchmark, BenchmarkCategory("JustAddOne")] 64 | public Task JustAddOne_Enumerable() 65 | { 66 | return ParallelAsync.ForEachAsync(InputNumbersEnumerable, TestFunctions.JustAddOne_WithCancellationToken, MaxBatchSize, AllowOutOfOrder, NumberOfItemsInCollection, CancellationToken.None); 67 | } 68 | 69 | [Benchmark, BenchmarkCategory("JustAddOne")] 70 | public Task JustAddOne_Array() 71 | { 72 | return ParallelAsync.ForEachAsync(InputNumbersArray, TestFunctions.JustAddOne_WithCancellationToken, MaxBatchSize, AllowOutOfOrder, NumberOfItemsInCollection, CancellationToken.None); 73 | } 74 | 75 | // ReturnTaskCompletedTask 76 | 77 | [Benchmark(Baseline = true), BenchmarkCategory("ReturnTaskCompletedTask")] 78 | public Task ReturnTaskCompletedTask_List() 79 | { 80 | return ParallelAsync.ForEachAsync(InputNumbersList, TestFunctions.ReturnCompletedTask_WithCancellationToken, MaxBatchSize, AllowOutOfOrder, CancellationToken.None); 81 | } 82 | 83 | [Benchmark, BenchmarkCategory("ReturnTaskCompletedTask")] 84 | public Task ReturnTaskCompletedTask_ReadOnlyList() 85 | { 86 | return ParallelAsync.ForEachAsync(InputNumbersReadOnlyList, TestFunctions.ReturnCompletedTask_WithCancellationToken, MaxBatchSize, AllowOutOfOrder, CancellationToken.None); 87 | } 88 | 89 | [Benchmark, BenchmarkCategory("ReturnTaskCompletedTask")] 90 | public Task ReturnTaskCompletedTask_Enumerable() 91 | { 92 | return ParallelAsync.ForEachAsync(InputNumbersEnumerable, TestFunctions.ReturnCompletedTask_WithCancellationToken, MaxBatchSize, AllowOutOfOrder, CancellationToken.None); 93 | } 94 | 95 | [Benchmark, BenchmarkCategory("ReturnTaskCompletedTask")] 96 | public Task ReturnTaskCompletedTask_Array() 97 | { 98 | return ParallelAsync.ForEachAsync(InputNumbersArray, TestFunctions.ReturnCompletedTask_WithCancellationToken, MaxBatchSize, AllowOutOfOrder, CancellationToken.None); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tests/CSRakowski.Parallel.Tests/ExtensionMethodsTests_IAsyncEnumerable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using CSRakowski.Parallel; 8 | using Xunit; 9 | using CSRakowski.Parallel.Extensions; 10 | using System.Threading; 11 | using CSRakowski.Parallel.Helpers; 12 | using CSRakowski.Parallel.Tests.Helpers; 13 | using CSRakowski.AsyncStreamsPreparations; 14 | 15 | namespace CSRakowski.Parallel.Tests 16 | { 17 | [Collection("ParallelAsync IAsyncEnumerable Extension Methods Tests")] 18 | public class ExtensionMethodsTests_IAsyncEnumerable 19 | { 20 | [Fact] 21 | public async Task ParallelAsync_Runs_With_Default_Settings() 22 | { 23 | var input = Enumerable.Range(1, 10).ToList().AsAsyncEnumerable(); 24 | 25 | var parallelAsync = input.AsParallelAsync(); 26 | 27 | Assert.NotNull(parallelAsync); 28 | 29 | var results = await parallelAsync.ForEachAsync((el) => Task.FromResult(el * 2)); 30 | 31 | Assert.NotNull(results); 32 | 33 | var list = results as List; 34 | 35 | Assert.NotNull(list); 36 | 37 | Assert.Equal(10, list.Count); 38 | 39 | for (int i = 0; i < list.Count; i++) 40 | { 41 | var expected = 2 * (1 + i); 42 | Assert.Equal(expected, list[i]); 43 | } 44 | } 45 | 46 | [Fact] 47 | public async Task ParallelAsync_Runs_With_Default_Settings2() 48 | { 49 | var input = Enumerable.Range(1, 10).ToList().AsAsyncEnumerable(); 50 | 51 | var parallelAsync = input.AsParallelAsync(); 52 | 53 | Assert.NotNull(parallelAsync); 54 | 55 | var results = await parallelAsync.ForEachAsync((el, ct) => Task.FromResult(el * 2)); 56 | 57 | Assert.NotNull(results); 58 | 59 | var list = results as List; 60 | 61 | Assert.NotNull(list); 62 | 63 | Assert.Equal(10, list.Count); 64 | 65 | for (int i = 0; i < list.Count; i++) 66 | { 67 | var expected = 2 * (1 + i); 68 | Assert.Equal(expected, list[i]); 69 | } 70 | } 71 | 72 | [Fact] 73 | public async Task ParallelAsync_Runs_With_Default_Settings3() 74 | { 75 | int sum = 0; 76 | int count = 0; 77 | 78 | var input = Enumerable.Range(1, 10).ToList().AsAsyncEnumerable(); 79 | 80 | var parallelAsync = input.AsParallelAsync(); 81 | 82 | Assert.NotNull(parallelAsync); 83 | 84 | await parallelAsync.ForEachAsync((el) => { 85 | Interlocked.Add(ref sum, el); 86 | Interlocked.Increment(ref count); 87 | 88 | return Task.CompletedTask; 89 | }); 90 | 91 | Assert.Equal(55, sum); 92 | Assert.Equal(10, count); 93 | } 94 | 95 | [Fact] 96 | public async Task ParallelAsync_Runs_With_Default_Settings4() 97 | { 98 | int sum = 0; 99 | int count = 0; 100 | 101 | var input = Enumerable.Range(1, 10).ToList().AsAsyncEnumerable(); 102 | 103 | var parallelAsync = input.AsParallelAsync(); 104 | 105 | Assert.NotNull(parallelAsync); 106 | 107 | await parallelAsync.ForEachAsync((el, ct) => { 108 | Interlocked.Add(ref sum, el); 109 | Interlocked.Increment(ref count); 110 | 111 | return Task.CompletedTask; 112 | }); 113 | 114 | Assert.Equal(55, sum); 115 | Assert.Equal(10, count); 116 | } 117 | 118 | [Fact] 119 | public async Task ParallelAsync_Supports_Full_Fluent_Usage() 120 | { 121 | var results = await Enumerable 122 | .Range(1, 10) 123 | .AsAsyncEnumerable() 124 | .AsParallelAsync() 125 | .WithEstimatedResultSize(10) 126 | .WithMaxDegreeOfParallelism(2) 127 | .WithOutOfOrderProcessing(false) 128 | .ForEachAsync((el) => Task.FromResult(el * 2), CancellationToken.None); 129 | 130 | Assert.NotNull(results); 131 | 132 | var list = results as List; 133 | 134 | Assert.NotNull(list); 135 | 136 | Assert.Equal(10, list.Count); 137 | 138 | for (int i = 0; i < list.Count; i++) 139 | { 140 | var expected = 2 * (1 + i); 141 | Assert.Equal(expected, list[i]); 142 | } 143 | } 144 | 145 | [Fact] 146 | public void ParallelAsync_Handles_Invalid_Input_As_Expected() 147 | { 148 | IAsyncEnumerable nullEnumerable = null; 149 | 150 | Assert.Throws(() => ParallelAsyncEx.AsParallelAsync(nullEnumerable)); 151 | 152 | var testCol = new List().AsAsyncEnumerable().AsParallelAsync(); 153 | 154 | Assert.Throws(() => ParallelAsyncEx.WithOutOfOrderProcessing(null, true)); 155 | 156 | Assert.Throws(() => ParallelAsyncEx.WithEstimatedResultSize(null, 1)); 157 | Assert.Throws(() => testCol.WithEstimatedResultSize(-1)); 158 | 159 | Assert.Throws(() => ParallelAsyncEx.WithMaxDegreeOfParallelism(null, 1)); 160 | Assert.Throws(() => testCol.WithMaxDegreeOfParallelism(-1)); 161 | } 162 | 163 | [Fact] 164 | public void ParallelAsync_Handles_Double_Calls_Correctly() 165 | { 166 | var testCol = new List().AsAsyncEnumerable().AsParallelAsync(); 167 | 168 | var testCol2 = testCol.AsParallelAsync(); 169 | 170 | Assert.Same(testCol, testCol2); 171 | } 172 | 173 | [Fact] 174 | public void ParallelAsync_IParallelAsyncEnumerable_Throws_NotSupportedException_When_Casted_Into_IEnumerable() 175 | { 176 | var input = Enumerable.Range(1, 10).ToList().AsAsyncEnumerable(); 177 | var testCol = input.AsParallelAsync(); 178 | 179 | Assert.Throws(() => testCol.GetEnumerator()); 180 | 181 | IEnumerable enumerable = testCol; 182 | 183 | Assert.Throws(() => enumerable.GetEnumerator()); 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /tests/CSRakowski.Parallel.Tests/ExtensionMethodsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using CSRakowski.Parallel; 8 | using Xunit; 9 | using CSRakowski.Parallel.Extensions; 10 | using System.Threading; 11 | using CSRakowski.Parallel.Tests.Helpers; 12 | 13 | namespace CSRakowski.Parallel.Tests 14 | { 15 | [Collection("ParallelAsync Extension Methods Tests")] 16 | public class ExtensionMethodsTests 17 | { 18 | [Fact] 19 | public async Task ParallelAsync_Runs_With_Default_Settings() 20 | { 21 | var input = Enumerable.Range(1, 10).ToList(); 22 | 23 | var parallelAsync = input.AsParallelAsync(); 24 | 25 | Assert.NotNull(parallelAsync); 26 | 27 | var results = await parallelAsync.ForEachAsync((el) => Task.FromResult(el * 2)); 28 | 29 | Assert.NotNull(results); 30 | 31 | var list = results as List; 32 | 33 | Assert.NotNull(list); 34 | 35 | Assert.Equal(input.Count, list.Count); 36 | 37 | for (int i = 0; i < list.Count; i++) 38 | { 39 | var expected = 2 * input[i]; 40 | Assert.Equal(expected, list[i]); 41 | } 42 | } 43 | 44 | [Fact] 45 | public async Task ParallelAsync_Runs_With_Default_Settings2() 46 | { 47 | var input = Enumerable.Range(1, 10).ToList(); 48 | 49 | var parallelAsync = input.AsParallelAsync(); 50 | 51 | Assert.NotNull(parallelAsync); 52 | 53 | var results = await parallelAsync.ForEachAsync((el, ct) => Task.FromResult(el * 2)); 54 | 55 | Assert.NotNull(results); 56 | 57 | var list = results as List; 58 | 59 | Assert.NotNull(list); 60 | 61 | Assert.Equal(input.Count, list.Count); 62 | 63 | for (int i = 0; i < list.Count; i++) 64 | { 65 | var expected = 2 * input[i]; 66 | Assert.Equal(expected, list[i]); 67 | } 68 | } 69 | 70 | [Fact] 71 | public async Task ParallelAsync_Runs_With_Default_Settings3() 72 | { 73 | int sum = 0; 74 | int count = 0; 75 | 76 | var input = Enumerable.Range(1, 10).ToList(); 77 | 78 | var parallelAsync = input.AsParallelAsync(); 79 | 80 | Assert.NotNull(parallelAsync); 81 | 82 | await parallelAsync.ForEachAsync((el) => { 83 | Interlocked.Add(ref sum, el); 84 | Interlocked.Increment(ref count); 85 | 86 | return Task.CompletedTask; 87 | }); 88 | 89 | Assert.Equal(55, sum); 90 | Assert.Equal(10, count); 91 | } 92 | 93 | [Fact] 94 | public async Task ParallelAsync_Runs_With_Default_Settings4() 95 | { 96 | int sum = 0; 97 | int count = 0; 98 | 99 | var input = Enumerable.Range(1, 10).ToList(); 100 | 101 | var parallelAsync = input.AsParallelAsync(); 102 | 103 | Assert.NotNull(parallelAsync); 104 | 105 | await parallelAsync.ForEachAsync((el, ct) => { 106 | Interlocked.Add(ref sum, el); 107 | Interlocked.Increment(ref count); 108 | 109 | return Task.CompletedTask; 110 | }); 111 | 112 | Assert.Equal(55, sum); 113 | Assert.Equal(10, count); 114 | } 115 | 116 | [Fact] 117 | public async Task ParallelAsync_Supports_Full_Fluent_Usage() 118 | { 119 | var results = await Enumerable 120 | .Range(1, 10) 121 | .AsParallelAsync() 122 | .WithEstimatedResultSize(10) 123 | .WithMaxDegreeOfParallelism(2) 124 | .WithOutOfOrderProcessing(false) 125 | .ForEachAsync((el) => Task.FromResult(el * 2), CancellationToken.None); 126 | 127 | Assert.NotNull(results); 128 | 129 | var list = results as List; 130 | 131 | Assert.NotNull(list); 132 | 133 | Assert.Equal(10, list.Count); 134 | 135 | for (int i = 0; i < list.Count; i++) 136 | { 137 | var expected = 2 * (1 + i); 138 | Assert.Equal(expected, list[i]); 139 | } 140 | } 141 | 142 | [Fact] 143 | public void ParallelAsync_Handles_Invalid_Input_As_Expected() 144 | { 145 | IEnumerable nullEnumerable = null; 146 | 147 | Assert.Throws(() => ParallelAsyncEx.AsParallelAsync(nullEnumerable)); 148 | 149 | var testCol = new List().AsParallelAsync(); 150 | 151 | Assert.Throws(() => ParallelAsyncEx.WithOutOfOrderProcessing(null, true)); 152 | 153 | Assert.Throws(() => ParallelAsyncEx.WithEstimatedResultSize(null, 1)); 154 | Assert.Throws(() => testCol.WithEstimatedResultSize(-1)); 155 | 156 | Assert.Throws(() => ParallelAsyncEx.WithMaxDegreeOfParallelism(null, 1)); 157 | Assert.Throws(() => testCol.WithMaxDegreeOfParallelism(-1)); 158 | } 159 | 160 | [Fact] 161 | public void ParallelAsync_Handles_Double_Calls_Correctly() 162 | { 163 | var testCol = new List().AsParallelAsync(); 164 | 165 | var testCol2 = testCol.AsParallelAsync(); 166 | 167 | Assert.Same(testCol, testCol2); 168 | } 169 | 170 | [Fact] 171 | public void ParallelAsync_IParallelAsyncEnumerable_Can_Still_Be_Casted_To_IEnumerable_Correctly() 172 | { 173 | var input = Enumerable.Range(1, 10).ToList(); 174 | var testCol = input.AsParallelAsync(); 175 | 176 | int count = 0; 177 | int sum = 0; 178 | 179 | foreach (var item in testCol) 180 | { 181 | Interlocked.Add(ref sum, item); 182 | Interlocked.Increment(ref count); 183 | } 184 | 185 | Assert.Equal(55, sum); 186 | Assert.Equal(10, count); 187 | 188 | 189 | IEnumerable enumerable = testCol; 190 | 191 | int count2 = 0; 192 | 193 | foreach (var item in enumerable) 194 | { 195 | Interlocked.Increment(ref count2); 196 | } 197 | 198 | Assert.Equal(10, count2); 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /tests/CSRakowski.Parallel.Benchmarks/Benchmarks/CompareWith_Parallel_ForEachAsync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using BenchmarkDotNet; 9 | using BenchmarkDotNet.Running; 10 | using BenchmarkDotNet.Reports; 11 | using BenchmarkDotNet.Attributes; 12 | using BenchmarkDotNet.Columns; 13 | using BenchmarkDotNet.Configs; 14 | using BenchmarkDotNet.Jobs; 15 | using CSRakowski.Parallel.Helpers; 16 | using CSRakowski.AsyncStreamsPreparations; 17 | using System.Collections.ObjectModel; 18 | 19 | namespace CSRakowski.Parallel.Benchmarks 20 | { 21 | #if NET6_0_OR_GREATER 22 | 23 | [MemoryDiagnoser] 24 | [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] 25 | [CategoriesColumn] 26 | [SimpleJob(RuntimeMoniker.Net60, baseline: true)] 27 | [SimpleJob(RuntimeMoniker.Net80, baseline: false)] 28 | public class CompareWith_Parallel_ForEachAsync 29 | { 30 | private const int NumberOfItemsInCollection = 10000; 31 | 32 | private readonly IEnumerable InputNumbers; 33 | private readonly IAsyncEnumerable InputNumbersAsync; 34 | 35 | public CompareWith_Parallel_ForEachAsync() 36 | { 37 | InputNumbers = Enumerable.Range(0, NumberOfItemsInCollection).ToList(); 38 | InputNumbersAsync = InputNumbers.AsAsyncEnumerable(); 39 | } 40 | 41 | [Params(1, 4, 8)] 42 | public int MaxBatchSize { get; set; } 43 | 44 | [Params(false, true)] 45 | public bool UseFrameworkImplementation { get; set; } 46 | 47 | [Benchmark, BenchmarkCategory("Compute_Double", "IEnumerable")] 48 | public async Task IEnumerable_Compute_Double() 49 | { 50 | if (UseFrameworkImplementation) 51 | { 52 | var concurrentResult = new System.Collections.Concurrent.ConcurrentBag(); 53 | 54 | var options = new System.Threading.Tasks.ParallelOptions 55 | { 56 | CancellationToken = CancellationToken.None, 57 | MaxDegreeOfParallelism = MaxBatchSize 58 | }; 59 | 60 | await System.Threading.Tasks.Parallel.ForEachAsync(InputNumbers, options, async (i, ct) => 61 | { 62 | var r = await TestFunctions.Compute_Double(i).ConfigureAwait(false); 63 | concurrentResult.Add(r); 64 | }).ConfigureAwait(false); 65 | } 66 | else 67 | { 68 | var total = await ParallelAsync.ForEachAsync(collection: InputNumbers, 69 | func: TestFunctions.Compute_Double, 70 | maxBatchSize: MaxBatchSize, 71 | allowOutOfOrderProcessing: true, 72 | estimatedResultSize: NumberOfItemsInCollection, 73 | cancellationToken: CancellationToken.None) 74 | .ConfigureAwait(false); 75 | } 76 | } 77 | 78 | [Benchmark, BenchmarkCategory("ReturnTaskCompletedTask", "IEnumerable")] 79 | public async Task IEnumerable_ReturnTaskCompletedTask() 80 | { 81 | if (UseFrameworkImplementation) 82 | { 83 | var concurrentResult = new System.Collections.Concurrent.ConcurrentBag(); 84 | 85 | var options = new System.Threading.Tasks.ParallelOptions 86 | { 87 | CancellationToken = CancellationToken.None, 88 | MaxDegreeOfParallelism = MaxBatchSize 89 | }; 90 | 91 | await System.Threading.Tasks.Parallel.ForEachAsync(InputNumbers, options, async (i, ct) => 92 | { 93 | await TestFunctions.ReturnCompletedTask(i).ConfigureAwait(false); 94 | }).ConfigureAwait(false); 95 | } 96 | else 97 | { 98 | await ParallelAsync.ForEachAsync(collection: InputNumbers, 99 | func: TestFunctions.ReturnCompletedTask, 100 | maxBatchSize: MaxBatchSize, 101 | allowOutOfOrderProcessing: true, 102 | cancellationToken: CancellationToken.None) 103 | .ConfigureAwait(false); 104 | } 105 | } 106 | 107 | [Benchmark, BenchmarkCategory("Compute_Double", "IAsyncEnumerable")] 108 | public async Task IAsyncEnumerable_Compute_Double() 109 | { 110 | if (UseFrameworkImplementation) 111 | { 112 | var concurrentResult = new System.Collections.Concurrent.ConcurrentBag(); 113 | 114 | var options = new System.Threading.Tasks.ParallelOptions 115 | { 116 | CancellationToken = CancellationToken.None, 117 | MaxDegreeOfParallelism = MaxBatchSize 118 | }; 119 | 120 | await System.Threading.Tasks.Parallel.ForEachAsync(InputNumbersAsync, options, async (i, ct) => 121 | { 122 | var r = await TestFunctions.Compute_Double(i).ConfigureAwait(false); 123 | concurrentResult.Add(r); 124 | }).ConfigureAwait(false); 125 | } 126 | else 127 | { 128 | var total = await ParallelAsync.ForEachAsync(collection: InputNumbersAsync, 129 | func: TestFunctions.Compute_Double, 130 | maxBatchSize: MaxBatchSize, 131 | allowOutOfOrderProcessing: true, 132 | estimatedResultSize: NumberOfItemsInCollection, 133 | cancellationToken: CancellationToken.None) 134 | .ConfigureAwait(false); 135 | } 136 | } 137 | 138 | [Benchmark, BenchmarkCategory("ReturnTaskCompletedTask", "IAsyncEnumerable")] 139 | public async Task IAsyncEnumerable_ReturnTaskCompletedTask() 140 | { 141 | if (UseFrameworkImplementation) 142 | { 143 | var concurrentResult = new System.Collections.Concurrent.ConcurrentBag(); 144 | 145 | var options = new System.Threading.Tasks.ParallelOptions 146 | { 147 | CancellationToken = CancellationToken.None, 148 | MaxDegreeOfParallelism = MaxBatchSize 149 | }; 150 | 151 | await System.Threading.Tasks.Parallel.ForEachAsync(InputNumbersAsync, options, async (i, ct) => 152 | { 153 | await TestFunctions.ReturnCompletedTask(i).ConfigureAwait(false); 154 | }).ConfigureAwait(false); 155 | } 156 | else 157 | { 158 | await ParallelAsync.ForEachAsync(collection: InputNumbersAsync, 159 | func: TestFunctions.ReturnCompletedTask, 160 | maxBatchSize: MaxBatchSize, 161 | allowOutOfOrderProcessing: true, 162 | cancellationToken: CancellationToken.None) 163 | .ConfigureAwait(false); 164 | } 165 | } 166 | } 167 | 168 | #endif 169 | 170 | } 171 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml 399 | -------------------------------------------------------------------------------- /tests/CSRakowski.Parallel.Tests/ParallelAsyncTests_AsyncStreams.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using CSRakowski.Parallel; 9 | using CSRakowski.Parallel.Helpers; 10 | using CSRakowski.Parallel.Tests.Helpers; 11 | using CSRakowski.AsyncStreamsPreparations; 12 | using CSRakowski.Parallel.Extensions; 13 | 14 | namespace CSRakowski.Parallel.Tests 15 | { 16 | [Collection("ParallelAsync AsyncStreams Tests")] 17 | public class ParallelAsyncTests_AsyncStreams 18 | { 19 | [Fact] 20 | public async Task ParallelAsync_Can_Process_IEnumerable_Streaming() 21 | { 22 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 23 | 24 | var asyncEnumerable = ParallelAsync.ForEachAsyncStream(input, (el) => Task.FromResult(el * 2), maxBatchSize: 1, estimatedResultSize: 10); 25 | 26 | int i = 0; 27 | await foreach (var el in asyncEnumerable) 28 | { 29 | var expected = 2 * (1 + i); 30 | Assert.Equal(expected, el); 31 | i++; 32 | } 33 | } 34 | 35 | [Fact] 36 | public async Task ParallelAsync_Can_Process_IAsyncEnumerable_Streaming() 37 | { 38 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }.AsAsyncEnumerable(); 39 | 40 | var asyncEnumerable = ParallelAsync.ForEachAsyncStream(input, (el) => Task.FromResult(el * 2), maxBatchSize: 1, estimatedResultSize: 10); 41 | 42 | int i = 0; 43 | await foreach (var el in asyncEnumerable) 44 | { 45 | var expected = 2 * (1 + i); 46 | Assert.Equal(expected, el); 47 | i++; 48 | } 49 | } 50 | 51 | [Fact] 52 | public async Task ParallelAsync_Can_Process_IEnumerable_Streaming_Using_Default_CancellationTokens() 53 | { 54 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 55 | 56 | int numberOfCalls = 0; 57 | 58 | var asyncEnumerable = ParallelAsync.ForEachAsyncStream(input, async (el, ct) => 59 | { 60 | await Task.Delay(500, ct); 61 | Interlocked.Increment(ref numberOfCalls); 62 | return el; 63 | }, maxBatchSize: 4, estimatedResultSize: 10); 64 | 65 | await foreach (var result in asyncEnumerable) 66 | { 67 | Assert.True((result > 0 && result < 11), "Expected a result between 1 and 10"); 68 | } 69 | 70 | Assert.Equal(10, numberOfCalls); 71 | } 72 | 73 | [Fact] 74 | public async Task ParallelAsync_Can_Process_IAsyncEnumerable_Streaming_Using_Default_CancellationTokens() 75 | { 76 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }.AsAsyncEnumerable(); 77 | 78 | int numberOfCalls = 0; 79 | 80 | var asyncEnumerable = ParallelAsync.ForEachAsyncStream(input, async (el, ct) => 81 | { 82 | await Task.Delay(500, ct); 83 | Interlocked.Increment(ref numberOfCalls); 84 | return el; 85 | }, maxBatchSize: 4, estimatedResultSize: 10); 86 | 87 | await foreach (var result in asyncEnumerable) 88 | { 89 | Assert.True((result > 0 && result < 11), "Expected a result between 1 and 10"); 90 | } 91 | 92 | Assert.Equal(10, numberOfCalls); 93 | } 94 | 95 | [Fact] 96 | public async Task ParallelAsync_Can_Process_IEnumerable_Streaming_Using_Provided_CancellationTokens() 97 | { 98 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 99 | 100 | var cts = new CancellationTokenSource(); 101 | var cancellationToken = cts.Token; 102 | 103 | int numberOfCalls = 0; 104 | 105 | cts.CancelAfter(250); 106 | 107 | var asyncEnumerable = ParallelAsync.ForEachAsyncStream(input, async (el, ct) => 108 | { 109 | await Task.Delay(500); 110 | 111 | Interlocked.Increment(ref numberOfCalls); 112 | return el; 113 | }, cancellationToken: cancellationToken, maxBatchSize: 1, estimatedResultSize: 10); 114 | 115 | var numberOfResults = 0; 116 | await foreach (var result in asyncEnumerable) 117 | { 118 | Assert.True((result > 0 && result < 11), "Expected a result between 1 and 10"); 119 | numberOfResults++; 120 | } 121 | 122 | Assert.True(numberOfCalls < 10); 123 | Assert.True(numberOfResults <= numberOfCalls, $"Expected less than, or equal to, {numberOfCalls}, but got {numberOfResults}"); 124 | 125 | cts.Dispose(); 126 | } 127 | 128 | [Fact] 129 | public async Task ParallelAsync_Can_Process_IAsyncEnumerable_Streaming_Using_Provided_CancellationTokens() 130 | { 131 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }.AsAsyncEnumerable(); 132 | 133 | var cts = new CancellationTokenSource(); 134 | var cancellationToken = cts.Token; 135 | 136 | int numberOfCalls = 0; 137 | 138 | cts.CancelAfter(250); 139 | 140 | var asyncEnumerable = ParallelAsync.ForEachAsyncStream(input, async (el, ct) => 141 | { 142 | await Task.Delay(500); 143 | 144 | Interlocked.Increment(ref numberOfCalls); 145 | return el; 146 | }, cancellationToken: cancellationToken, maxBatchSize: 1, estimatedResultSize: 10); 147 | 148 | var numberOfResults = 0; 149 | await foreach (var result in asyncEnumerable) 150 | { 151 | Assert.True((result > 0 && result < 11), "Expected a result between 1 and 10"); 152 | numberOfResults++; 153 | } 154 | 155 | Assert.True(numberOfCalls < 10); 156 | Assert.True(numberOfResults <= numberOfCalls, $"Expected less than, or equal to, {numberOfCalls}, but got {numberOfResults}"); 157 | 158 | cts.Dispose(); 159 | } 160 | 161 | [Fact] 162 | public async Task ParallelAsync_Throws_On_Invalid_Inputs() 163 | { 164 | var empty = new int[0]; 165 | IEnumerable nullEnumerable = null; 166 | 167 | var emptyAsync = new int[0].AsAsyncEnumerable(); 168 | IAsyncEnumerable nullAsyncEnumerable = null; 169 | 170 | var ex1 = await Assert.ThrowsAsync(async () => 171 | { 172 | await foreach (var i in ParallelAsync.ForEachAsyncStream(nullEnumerable, (e) => Task.FromResult(e))) 173 | { 174 | } 175 | }); 176 | var ex2 = await Assert.ThrowsAsync(async () => 177 | { 178 | await foreach (var i in ParallelAsync.ForEachAsyncStream(nullEnumerable, (e, ct) => Task.FromResult(e))) 179 | { 180 | } 181 | }); 182 | 183 | var ex3 = await Assert.ThrowsAsync(async () => 184 | { 185 | await foreach (var i in ParallelAsync.ForEachAsyncStream(nullAsyncEnumerable, (e) => Task.FromResult(e))) 186 | { 187 | } 188 | }); 189 | var ex4 = await Assert.ThrowsAsync(async () => 190 | { 191 | await foreach (var i in ParallelAsync.ForEachAsyncStream(nullAsyncEnumerable, (e, ct) => Task.FromResult(e))) 192 | { 193 | } 194 | }); 195 | 196 | var ex5 = await Assert.ThrowsAsync(async () => 197 | { 198 | await foreach (var i in ParallelAsync.ForEachAsyncStream(empty, (Func>)null)) 199 | { 200 | } 201 | }); 202 | var ex6 = await Assert.ThrowsAsync(async () => 203 | { 204 | await foreach (var i in ParallelAsync.ForEachAsyncStream(empty, (Func>)null)) 205 | { 206 | } 207 | }); 208 | 209 | var ex7 = await Assert.ThrowsAsync(async () => 210 | { 211 | await foreach (var i in ParallelAsync.ForEachAsyncStream(emptyAsync, (Func>)null)) 212 | { 213 | } 214 | }); 215 | var ex8 = await Assert.ThrowsAsync(async () => 216 | { 217 | await foreach (var i in ParallelAsync.ForEachAsyncStream(emptyAsync, (Func>)null)) 218 | { 219 | } 220 | }); 221 | 222 | var ex9 = await Assert.ThrowsAsync(async () => 223 | { 224 | await foreach (var i in ParallelAsync.ForEachAsyncStream(empty, (e, ct) => Task.FromResult(e), -1)) 225 | { 226 | } 227 | }); 228 | var ex10 = await Assert.ThrowsAsync(async () => 229 | { 230 | await foreach (var i in ParallelAsync.ForEachAsyncStream(emptyAsync, (e, ct) => Task.FromResult(e), -1)) 231 | { 232 | } 233 | }); 234 | 235 | Assert.Equal("collection", ex1.ParamName); 236 | Assert.Equal("collection", ex2.ParamName); 237 | Assert.Equal("collection", ex3.ParamName); 238 | Assert.Equal("collection", ex4.ParamName); 239 | 240 | Assert.Equal("func", ex5.ParamName); 241 | Assert.Equal("func", ex6.ParamName); 242 | Assert.Equal("func", ex7.ParamName); 243 | Assert.Equal("func", ex8.ParamName); 244 | 245 | Assert.Equal("maxBatchSize", ex9.ParamName); 246 | Assert.Equal("maxBatchSize", ex10.ParamName); 247 | } 248 | 249 | [Fact] 250 | public async Task ParallelAsync_Can_Chain_Together_AsyncStreams() 251 | { 252 | var input = Enumerable.Range(1, 40).ToList().AsAsyncEnumerable(); 253 | 254 | var intermediateResult = ParallelAsync.ForEachAsyncStream(input, (el) => Task.FromResult(el * 2), maxBatchSize: 3, estimatedResultSize: 10); 255 | 256 | var asyncEnumerable = ParallelAsync.ForEachAsyncStream(intermediateResult, (el) => Task.FromResult(el * 2), maxBatchSize: 3, estimatedResultSize: 10); 257 | 258 | int i = 0; 259 | await foreach (var el in asyncEnumerable) 260 | { 261 | var expected = 4 * (1 + i); 262 | Assert.Equal(expected, el); 263 | i++; 264 | } 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/CSRakowski.Parallel/ParallelAsync.Unordered.cs: -------------------------------------------------------------------------------- 1 | using CSRakowski.Parallel.Helpers; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace CSRakowski.Parallel 10 | { 11 | public static partial class ParallelAsync 12 | { 13 | #region IEnumerable 14 | 15 | /// 16 | /// Implementation to run the specified async method for each item of the input collection in an batched manner, allowing out of order processing. 17 | /// 18 | /// The result item type 19 | /// The input item type 20 | /// The collection of items to use as input arguments 21 | /// The async method to run for each item 22 | /// The batch size to use 23 | /// The estimated size of the result collection. 24 | /// A 25 | /// The results of the operations 26 | private static async Task> ForEachAsyncImplUnordered(IEnumerable collection, Func> func, int batchSize, int estimatedResultSize, CancellationToken cancellationToken) 27 | { 28 | var result = ListHelpers.GetList(collection, estimatedResultSize); 29 | 30 | long runId = ParallelAsyncEventSource.Log.GetRunId(); 31 | ParallelAsyncEventSource.Log.RunStart(runId, batchSize, true, estimatedResultSize); 32 | 33 | using (var enumerator = collection.GetEnumerator()) 34 | { 35 | var hasNext = true; 36 | int batchId = 0; 37 | var taskList = ListHelpers.GetList>(batchSize); 38 | 39 | while (!cancellationToken.IsCancellationRequested) 40 | { 41 | // check the hasNext from the previous run, if false; don't call MoveNext() again 42 | // call MoveNext() and assign it to the hasNext variable, then check if this run still had a next 43 | if (hasNext && (hasNext = enumerator.MoveNext())) 44 | { 45 | var element = enumerator.Current; 46 | 47 | var task = func(element, cancellationToken); 48 | taskList.Add(task); 49 | 50 | if (taskList.Count < batchSize) 51 | { 52 | continue; 53 | } 54 | } 55 | 56 | if (!hasNext && taskList.Count == 0) 57 | { 58 | break; 59 | } 60 | 61 | ParallelAsyncEventSource.Log.BatchStart(runId, batchId, taskList.Count); 62 | 63 | await Task.WhenAny(taskList).ConfigureAwait(false); 64 | 65 | #pragma warning disable PH_S026 // Blocking Wait in Async Method 66 | #pragma warning disable AsyncFixer02 // Long-running or blocking operations inside an async method 67 | 68 | var completed = taskList.FindAll(t => t.IsCompleted); 69 | foreach (var t in completed) 70 | { 71 | result.Add(t.Result); 72 | taskList.Remove(t); 73 | } 74 | 75 | #pragma warning restore AsyncFixer02 // Long-running or blocking operations inside an async method 76 | #pragma warning restore PH_S026 // Blocking Wait in Async Method 77 | 78 | ParallelAsyncEventSource.Log.BatchStop(runId, batchId); 79 | 80 | batchId++; 81 | } 82 | } 83 | 84 | ParallelAsyncEventSource.Log.RunStop(runId); 85 | 86 | return result; 87 | } 88 | 89 | /// 90 | /// Implementation to run the specified async method for each item of the input collection in an batched manner, allowing out of order processing. 91 | /// 92 | /// The input item type 93 | /// The collection of items to use as input arguments 94 | /// The async method to run for each item 95 | /// The batch size to use 96 | /// A 97 | /// A signaling completion 98 | private static async Task ForEachAsyncImplUnordered(IEnumerable collection, Func func, int batchSize, CancellationToken cancellationToken) 99 | { 100 | long runId = ParallelAsyncEventSource.Log.GetRunId(); 101 | ParallelAsyncEventSource.Log.RunStart(runId, batchSize, true, 0); 102 | 103 | using (var enumerator = collection.GetEnumerator()) 104 | { 105 | var hasNext = true; 106 | int batchId = 0; 107 | var taskList = ListHelpers.GetList(batchSize); 108 | 109 | while (!cancellationToken.IsCancellationRequested) 110 | { 111 | // check the hasNext from the previous run, if false; don't call MoveNext() again 112 | // call MoveNext() and assign it to the hasNext variable, then check if this run still had a next 113 | if (hasNext && (hasNext = enumerator.MoveNext())) 114 | { 115 | var element = enumerator.Current; 116 | 117 | var task = func(element, cancellationToken); 118 | taskList.Add(task); 119 | 120 | if (taskList.Count < batchSize) 121 | { 122 | continue; 123 | } 124 | } 125 | 126 | if (!hasNext && taskList.Count == 0) 127 | { 128 | break; 129 | } 130 | 131 | ParallelAsyncEventSource.Log.BatchStart(runId, batchId, taskList.Count); 132 | 133 | await Task.WhenAny(taskList).ConfigureAwait(false); 134 | 135 | taskList.RemoveAll(t => t.IsCompleted); 136 | 137 | ParallelAsyncEventSource.Log.BatchStop(runId, batchId); 138 | 139 | batchId++; 140 | } 141 | } 142 | 143 | ParallelAsyncEventSource.Log.RunStop(runId); 144 | } 145 | 146 | #endregion IEnumerable 147 | 148 | #region IAsyncEnumerable 149 | 150 | private static async Task> ForEachAsyncImplUnordered(IAsyncEnumerable collection, Func> func, int batchSize, int estimatedResultSize, CancellationToken cancellationToken) 151 | { 152 | var result = ListHelpers.GetList(estimatedResultSize); 153 | 154 | long runId = ParallelAsyncEventSource.Log.GetRunId(); 155 | ParallelAsyncEventSource.Log.RunStart(runId, batchSize, true, estimatedResultSize); 156 | 157 | var enumerator = collection.GetAsyncEnumerator(cancellationToken); 158 | try 159 | { 160 | var hasNext = true; 161 | int batchId = 0; 162 | var taskList = ListHelpers.GetList>(batchSize); 163 | 164 | while (!cancellationToken.IsCancellationRequested) 165 | { 166 | // check the hasNext from the previous run, if false; don't call MoveNext() again 167 | // call MoveNext() and assign it to the hasNext variable, then check if this run still had a next 168 | if (hasNext && (hasNext = await enumerator.MoveNextAsync().ConfigureAwait(false))) 169 | { 170 | var element = enumerator.Current; 171 | 172 | var task = func(element, cancellationToken); 173 | taskList.Add(task); 174 | 175 | if (taskList.Count < batchSize) 176 | { 177 | continue; 178 | } 179 | } 180 | 181 | if (!hasNext && taskList.Count == 0) 182 | { 183 | break; 184 | } 185 | 186 | ParallelAsyncEventSource.Log.BatchStart(runId, batchId, taskList.Count); 187 | 188 | await Task.WhenAny(taskList).ConfigureAwait(false); 189 | 190 | #pragma warning disable PH_S026 // Blocking Wait in Async Method 191 | #pragma warning disable AsyncFixer02 // Long-running or blocking operations inside an async method 192 | 193 | var completed = taskList.FindAll(t => t.IsCompleted); 194 | foreach (var t in completed) 195 | { 196 | result.Add(t.Result); 197 | taskList.Remove(t); 198 | } 199 | 200 | #pragma warning restore AsyncFixer02 // Long-running or blocking operations inside an async method 201 | #pragma warning restore PH_S026 // Blocking Wait in Async Method 202 | 203 | ParallelAsyncEventSource.Log.BatchStop(runId, batchId); 204 | 205 | batchId++; 206 | } 207 | } 208 | finally 209 | { 210 | await enumerator.DisposeAsync().ConfigureAwait(false); 211 | } 212 | 213 | ParallelAsyncEventSource.Log.RunStop(runId); 214 | 215 | return result; 216 | } 217 | 218 | private static async Task ForEachAsyncImplUnordered(IAsyncEnumerable collection, Func func, int batchSize, CancellationToken cancellationToken) 219 | { 220 | long runId = ParallelAsyncEventSource.Log.GetRunId(); 221 | ParallelAsyncEventSource.Log.RunStart(runId, batchSize, true, 0); 222 | 223 | var enumerator = collection.GetAsyncEnumerator(cancellationToken); 224 | try 225 | { 226 | var hasNext = true; 227 | int batchId = 0; 228 | var taskList = ListHelpers.GetList(batchSize); 229 | 230 | while (!cancellationToken.IsCancellationRequested) 231 | { 232 | // check the hasNext from the previous run, if false; don't call MoveNext() again 233 | // call MoveNext() and assign it to the hasNext variable, then check if this run still had a next 234 | if (hasNext && (hasNext = await enumerator.MoveNextAsync().ConfigureAwait(false))) 235 | { 236 | var element = enumerator.Current; 237 | 238 | var task = func(element, cancellationToken); 239 | taskList.Add(task); 240 | 241 | if (taskList.Count < batchSize) 242 | { 243 | continue; 244 | } 245 | } 246 | 247 | if (!hasNext && taskList.Count == 0) 248 | { 249 | break; 250 | } 251 | 252 | ParallelAsyncEventSource.Log.BatchStart(runId, batchId, taskList.Count); 253 | 254 | await Task.WhenAny(taskList).ConfigureAwait(false); 255 | 256 | taskList.RemoveAll(t => t.IsCompleted); 257 | 258 | ParallelAsyncEventSource.Log.BatchStop(runId, batchId); 259 | 260 | batchId++; 261 | } 262 | } 263 | finally 264 | { 265 | await enumerator.DisposeAsync().ConfigureAwait(false); 266 | } 267 | 268 | ParallelAsyncEventSource.Log.RunStop(runId); 269 | } 270 | 271 | #endregion IAsyncEnumerable 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /tests/CSRakowski.Parallel.Tests/ParallelAsyncTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using CSRakowski.Parallel; 9 | using CSRakowski.Parallel.Tests.Helpers; 10 | 11 | namespace CSRakowski.Parallel.Tests 12 | { 13 | [Collection("ParallelAsync Base Tests")] 14 | public class ParallelAsyncTests 15 | { 16 | [Fact] 17 | public async Task ParallelAsync_Can_Batch_Basic_Work() 18 | { 19 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 20 | 21 | var results = await ParallelAsync.ForEachAsync(input, (el) => Task.FromResult(el * 2), maxBatchSize: 1, estimatedResultSize: input.Length); 22 | 23 | Assert.NotNull(results); 24 | 25 | var list = results as List; 26 | 27 | Assert.NotNull(list); 28 | 29 | Assert.Equal(input.Length, list.Count); 30 | 31 | for (int i = 0; i < list.Count; i++) 32 | { 33 | var expected = 2 * input[i]; 34 | Assert.Equal(expected, list[i]); 35 | } 36 | } 37 | 38 | [Fact] 39 | public async Task ParallelAsync_Can_Batch_Basic_Work_Void() 40 | { 41 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 42 | 43 | await ParallelAsync.ForEachAsync(input, (el) => 44 | { 45 | return Task.CompletedTask; 46 | }, maxBatchSize: 1); 47 | 48 | Assert.True(true); 49 | } 50 | 51 | [Fact] 52 | public async Task ParallelAsync_Can_Handle_Misaligned_Sizing() 53 | { 54 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 55 | 56 | var cancellationToken = CancellationToken.None; 57 | 58 | var results = await ParallelAsync.ForEachAsync(input, (el) => Task.FromResult(el * 2), maxBatchSize: 4, estimatedResultSize: input.Length, cancellationToken: cancellationToken); 59 | 60 | Assert.NotNull(results); 61 | 62 | var list = results as List; 63 | 64 | Assert.NotNull(list); 65 | 66 | Assert.Equal(input.Length, list.Count); 67 | 68 | for (int i = 0; i < list.Count; i++) 69 | { 70 | var expected = 2 * input[i]; 71 | Assert.Equal(expected, list[i]); 72 | } 73 | } 74 | 75 | [Fact] 76 | public async Task ParallelAsync_Can_Handle_Misaligned_Sizing_Without_EstimatedSize() 77 | { 78 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 79 | 80 | var cancellationToken = CancellationToken.None; 81 | 82 | var results = await ParallelAsync.ForEachAsync(input, (el) => Task.FromResult(el * 2), maxBatchSize: 4, cancellationToken: cancellationToken); 83 | 84 | Assert.NotNull(results); 85 | 86 | var list = results as List; 87 | 88 | Assert.NotNull(list); 89 | 90 | Assert.Equal(input.Length, list.Count); 91 | 92 | for (int i = 0; i < list.Count; i++) 93 | { 94 | var expected = 2 * input[i]; 95 | Assert.Equal(expected, list[i]); 96 | } 97 | } 98 | 99 | [Fact] 100 | public async Task ParallelAsync_Can_Handle_Misaligned_Sizing_Void() 101 | { 102 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 103 | 104 | var cancellationToken = CancellationToken.None; 105 | 106 | await ParallelAsync.ForEachAsync(input, (el) => 107 | { 108 | return Task.CompletedTask; 109 | }, maxBatchSize: 4, cancellationToken: cancellationToken); 110 | 111 | Assert.True(true); 112 | } 113 | 114 | [Fact] 115 | public async Task ParallelAsync_Can_Handle_Misaligned_Sizing_Void_Without_EstimatedSize() 116 | { 117 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 118 | 119 | var cancellationToken = CancellationToken.None; 120 | 121 | await ParallelAsync.ForEachAsync(input, (el) => 122 | { 123 | return Task.CompletedTask; 124 | }, maxBatchSize: 4, cancellationToken: cancellationToken); 125 | 126 | Assert.True(true); 127 | } 128 | 129 | [Fact] 130 | public async Task ParallelAsync_Can_Handle_Propagating_CancellationTokens() 131 | { 132 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 133 | 134 | var cts = new CancellationTokenSource(); 135 | var cancellationToken = cts.Token; 136 | 137 | int numberOfCalls = 0; 138 | 139 | cts.CancelAfter(250); 140 | 141 | await ParallelAsync.ForEachAsync(input, async (el) => 142 | { 143 | await Task.Delay(500); 144 | Interlocked.Increment(ref numberOfCalls); 145 | }, cancellationToken: cancellationToken, maxBatchSize: 4); 146 | 147 | Assert.True(numberOfCalls < 10); 148 | 149 | cts.Dispose(); 150 | } 151 | 152 | [Fact] 153 | public async Task ParallelAsync_Can_Handle_Using_Default_CancellationTokens() 154 | { 155 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 156 | 157 | int numberOfCalls = 0; 158 | 159 | await ParallelAsync.ForEachAsync(input, async (el, ct) => 160 | { 161 | await Task.Delay(500, ct); 162 | Interlocked.Increment(ref numberOfCalls); 163 | }); 164 | 165 | Assert.Equal(10, numberOfCalls); 166 | } 167 | 168 | [Fact] 169 | public async Task ParallelAsync_TaskT_Can_Handle_Propagating_CancellationTokens() 170 | { 171 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 172 | 173 | var cts = new CancellationTokenSource(); 174 | var cancellationToken = cts.Token; 175 | 176 | int numberOfCalls = 0; 177 | 178 | cts.CancelAfter(250); 179 | 180 | var results = await ParallelAsync.ForEachAsync(input, async (el) => 181 | { 182 | await Task.Delay(500); 183 | 184 | Interlocked.Increment(ref numberOfCalls); 185 | return el; 186 | }, cancellationToken: cancellationToken, maxBatchSize: 4); 187 | 188 | Assert.True(numberOfCalls < 10); 189 | var numberOfResults = results.Count(); 190 | Assert.True(numberOfResults <= numberOfCalls, $"Expected less than, or equal to, {numberOfCalls}, but got {numberOfResults}"); 191 | 192 | cts.Dispose(); 193 | } 194 | 195 | [Fact] 196 | public async Task ParallelAsync_TaskT_No_Batching() 197 | { 198 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 199 | 200 | var cts = new CancellationTokenSource(); 201 | var cancellationToken = cts.Token; 202 | 203 | int numberOfCalls = 0; 204 | 205 | var results = await ParallelAsync.ForEachAsync(input, (el, ct) => 206 | { 207 | Interlocked.Increment(ref numberOfCalls); 208 | 209 | return Task.FromResult(el); 210 | }, maxBatchSize: 1, cancellationToken: cancellationToken); 211 | 212 | Assert.Equal(10, numberOfCalls); 213 | Assert.Equal(numberOfCalls, results.Count()); 214 | 215 | cts.Dispose(); 216 | } 217 | 218 | [Fact] 219 | public async Task ParallelAsync_Throws_On_Invalid_Inputs() 220 | { 221 | var empty = new int[0]; 222 | IEnumerable nullEnumerable = null; 223 | 224 | var ex1 = await Assert.ThrowsAsync(() => ParallelAsync.ForEachAsync(nullEnumerable, (e) => Task.CompletedTask)); 225 | var ex2 = await Assert.ThrowsAsync(() => ParallelAsync.ForEachAsync(nullEnumerable, (e, ct) => Task.CompletedTask)); 226 | 227 | var ex3 = await Assert.ThrowsAsync(() => ParallelAsync.ForEachAsync(nullEnumerable, (e) => Task.FromResult(e))); 228 | var ex4 = await Assert.ThrowsAsync(() => ParallelAsync.ForEachAsync(nullEnumerable, (e, ct) => Task.FromResult(e))); 229 | 230 | var ex5 = await Assert.ThrowsAsync(() => ParallelAsync.ForEachAsync(empty, (Func)null)); 231 | var ex6 = await Assert.ThrowsAsync(() => ParallelAsync.ForEachAsync(empty, (Func)null)); 232 | 233 | var ex7 = await Assert.ThrowsAsync(() => ParallelAsync.ForEachAsync(empty, (Func>)null)); 234 | var ex8 = await Assert.ThrowsAsync(() => ParallelAsync.ForEachAsync(empty, (Func>)null)); 235 | 236 | var ex9 = await Assert.ThrowsAsync(() => ParallelAsync.ForEachAsync(empty, (e) => Task.CompletedTask, -1)); 237 | 238 | Assert.Equal("collection", ex1.ParamName); 239 | Assert.Equal("collection", ex2.ParamName); 240 | Assert.Equal("collection", ex3.ParamName); 241 | Assert.Equal("collection", ex4.ParamName); 242 | 243 | Assert.Equal("func", ex5.ParamName); 244 | Assert.Equal("func", ex6.ParamName); 245 | Assert.Equal("func", ex7.ParamName); 246 | Assert.Equal("func", ex8.ParamName); 247 | 248 | Assert.Equal("maxBatchSize", ex9.ParamName); 249 | } 250 | 251 | [Fact] 252 | public async Task ParallelAsync_Can_Batch_Basic_Work_Unordered() 253 | { 254 | const int numberOfElements = 100; 255 | var callCount = 0; 256 | var input = Enumerable.Range(1, numberOfElements).ToArray(); 257 | 258 | var results = await ParallelAsync.ForEachAsync(input, (el) => 259 | { 260 | var r = el + Interlocked.Increment(ref callCount); 261 | 262 | return Task.FromResult(r); 263 | }, maxBatchSize: 9, allowOutOfOrderProcessing: true, estimatedResultSize: input.Length); 264 | 265 | Assert.Equal(numberOfElements, callCount); 266 | Assert.Equal(numberOfElements, results.Count()); 267 | } 268 | 269 | [Fact] 270 | public async Task ParallelAsync_Can_Batch_Basic_Work_Void_Unordered() 271 | { 272 | const int numberOfElements = 100; 273 | var callCount = 0; 274 | var input = Enumerable.Range(1, numberOfElements).ToArray(); 275 | 276 | await ParallelAsync.ForEachAsync(input, (el) => 277 | { 278 | var r = el + Interlocked.Increment(ref callCount); 279 | 280 | return Task.CompletedTask; 281 | }, maxBatchSize: 9, allowOutOfOrderProcessing: true); 282 | 283 | Assert.Equal(numberOfElements, callCount); 284 | } 285 | 286 | [Fact] 287 | public async Task ParallelAsync_Can_Handle_Propagating_CancellationTokens_Unordered() 288 | { 289 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 290 | 291 | var cts = new CancellationTokenSource(); 292 | var cancellationToken = cts.Token; 293 | 294 | int numberOfCalls = 0; 295 | 296 | cts.CancelAfter(250); 297 | 298 | await ParallelAsync.ForEachAsync(input, async (el) => 299 | { 300 | await Task.Delay(500); 301 | Interlocked.Increment(ref numberOfCalls); 302 | }, allowOutOfOrderProcessing: true, maxBatchSize: 4, cancellationToken: cancellationToken); 303 | 304 | Assert.True(numberOfCalls < 10); 305 | 306 | cts.Dispose(); 307 | } 308 | 309 | [Fact] 310 | public async Task ParallelAsync_TaskT_Can_Handle_Propagating_CancellationTokens_Unordered() 311 | { 312 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 313 | 314 | var cts = new CancellationTokenSource(); 315 | var cancellationToken = cts.Token; 316 | 317 | int numberOfCalls = 0; 318 | 319 | cts.CancelAfter(250); 320 | 321 | var results = await ParallelAsync.ForEachAsync(input, async (el) => 322 | { 323 | await Task.Delay(500); 324 | 325 | Interlocked.Increment(ref numberOfCalls); 326 | return el; 327 | }, allowOutOfOrderProcessing: true, maxBatchSize: 4, cancellationToken: cancellationToken); 328 | 329 | Assert.True(numberOfCalls < 10); 330 | var numberOfResults = results.Count(); 331 | Assert.True(numberOfResults <= numberOfCalls, $"Expected less than, or equal to, {numberOfCalls}, but got {numberOfResults}"); 332 | 333 | cts.Dispose(); 334 | } 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /tests/CSRakowski.Parallel.Tests/ParallelAsyncTests_IAsyncEnumerable.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using CSRakowski.Parallel; 9 | using CSRakowski.Parallel.Helpers; 10 | using CSRakowski.Parallel.Tests.Helpers; 11 | using CSRakowski.AsyncStreamsPreparations; 12 | 13 | namespace CSRakowski.Parallel.Tests 14 | { 15 | [Collection("ParallelAsync IAsyncEnumerable Tests")] 16 | public class ParallelAsyncTests_IAsyncEnumerable 17 | { 18 | [Fact] 19 | public async Task ParallelAsync_Can_Batch_Basic_Work() 20 | { 21 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }.AsAsyncEnumerable(); 22 | 23 | var results = await ParallelAsync.ForEachAsync(input, (el) => Task.FromResult(el * 2), maxBatchSize: 1, estimatedResultSize: 10); 24 | 25 | Assert.NotNull(results); 26 | 27 | var list = results as List; 28 | 29 | Assert.NotNull(list); 30 | 31 | Assert.Equal(10, list.Count); 32 | 33 | for (int i = 0; i < list.Count; i++) 34 | { 35 | var expected = 2 * (1 + i); 36 | Assert.Equal(expected, list[i]); 37 | } 38 | } 39 | 40 | [Fact] 41 | public async Task ParallelAsync_Can_Batch_Basic_Work_Void() 42 | { 43 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }.AsAsyncEnumerable(); 44 | 45 | await ParallelAsync.ForEachAsync(input, (el) => 46 | { 47 | return Task.CompletedTask; 48 | }, maxBatchSize: 1); 49 | 50 | Assert.True(true); 51 | } 52 | 53 | [Fact] 54 | public async Task ParallelAsync_Can_Handle_Misaligned_Sizing() 55 | { 56 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }.AsAsyncEnumerable(); 57 | 58 | var cancellationToken = CancellationToken.None; 59 | 60 | var results = await ParallelAsync.ForEachAsync(input, (el) => Task.FromResult(el * 2), maxBatchSize: 4, estimatedResultSize: 10, cancellationToken: cancellationToken); 61 | 62 | Assert.NotNull(results); 63 | 64 | var list = results as List; 65 | 66 | Assert.NotNull(list); 67 | 68 | Assert.Equal(10, list.Count); 69 | 70 | for (int i = 0; i < list.Count; i++) 71 | { 72 | var expected = 2 * (1 + i); 73 | Assert.Equal(expected, list[i]); 74 | } 75 | } 76 | 77 | [Fact] 78 | public async Task ParallelAsync_Can_Handle_Misaligned_Sizing_Without_EstimatedSize() 79 | { 80 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }.AsAsyncEnumerable(); 81 | 82 | var cancellationToken = CancellationToken.None; 83 | 84 | var results = await ParallelAsync.ForEachAsync(input, (el) => Task.FromResult(el * 2), maxBatchSize: 4, cancellationToken: cancellationToken); 85 | 86 | Assert.NotNull(results); 87 | 88 | var list = results as List; 89 | 90 | Assert.NotNull(list); 91 | 92 | Assert.Equal(10, list.Count); 93 | 94 | for (int i = 0; i < list.Count; i++) 95 | { 96 | var expected = 2 * (1 + i); 97 | Assert.Equal(expected, list[i]); 98 | } 99 | } 100 | 101 | [Fact] 102 | public async Task ParallelAsync_Can_Handle_Misaligned_Sizing_Void() 103 | { 104 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }.AsAsyncEnumerable(); 105 | 106 | var cancellationToken = CancellationToken.None; 107 | 108 | await ParallelAsync.ForEachAsync(input, (el) => 109 | { 110 | return Task.CompletedTask; 111 | }, maxBatchSize: 4, cancellationToken: cancellationToken); 112 | 113 | Assert.True(true); 114 | } 115 | 116 | [Fact] 117 | public async Task ParallelAsync_Can_Handle_Misaligned_Sizing_Void_Without_EstimatedSize() 118 | { 119 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }.AsAsyncEnumerable(); 120 | 121 | var cancellationToken = CancellationToken.None; 122 | 123 | await ParallelAsync.ForEachAsync(input, (el) => 124 | { 125 | return Task.CompletedTask; 126 | }, maxBatchSize: 4, cancellationToken: cancellationToken); 127 | 128 | Assert.True(true); 129 | } 130 | 131 | [Fact] 132 | public async Task ParallelAsync_Can_Handle_Propagating_CancellationTokens() 133 | { 134 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }.AsAsyncEnumerable(); 135 | 136 | var cts = new CancellationTokenSource(); 137 | var cancellationToken = cts.Token; 138 | 139 | int numberOfCalls = 0; 140 | 141 | cts.CancelAfter(250); 142 | 143 | await ParallelAsync.ForEachAsync(input, async (el) => 144 | { 145 | await Task.Delay(500); 146 | Interlocked.Increment(ref numberOfCalls); 147 | }, cancellationToken: cancellationToken, maxBatchSize: 4); 148 | 149 | Assert.True(numberOfCalls < 10); 150 | 151 | cts.Dispose(); 152 | } 153 | 154 | [Fact] 155 | public async Task ParallelAsync_Can_Handle_Using_Default_CancellationTokens() 156 | { 157 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }.AsAsyncEnumerable(); 158 | 159 | int numberOfCalls = 0; 160 | 161 | await ParallelAsync.ForEachAsync(input, async (el, ct) => 162 | { 163 | await Task.Delay(500, ct); 164 | Interlocked.Increment(ref numberOfCalls); 165 | }); 166 | 167 | Assert.Equal(10, numberOfCalls); 168 | } 169 | 170 | [Fact] 171 | public async Task ParallelAsync_TaskT_Can_Handle_Propagating_CancellationTokens() 172 | { 173 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }.AsAsyncEnumerable(); 174 | 175 | var cts = new CancellationTokenSource(); 176 | var cancellationToken = cts.Token; 177 | 178 | int numberOfCalls = 0; 179 | 180 | cts.CancelAfter(250); 181 | 182 | var results = await ParallelAsync.ForEachAsync(input, async (el) => 183 | { 184 | await Task.Delay(500); 185 | 186 | Interlocked.Increment(ref numberOfCalls); 187 | return el; 188 | }, cancellationToken: cancellationToken, maxBatchSize: 4); 189 | 190 | Assert.True(numberOfCalls < 10); 191 | var numberOfResults = results.Count(); 192 | Assert.True(numberOfResults <= numberOfCalls, $"Expected less than, or equal to, {numberOfCalls}, but got {numberOfResults}"); 193 | 194 | cts.Dispose(); 195 | } 196 | 197 | [Fact] 198 | public async Task ParallelAsync_TaskT_No_Batching() 199 | { 200 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }.AsAsyncEnumerable(); 201 | 202 | var cts = new CancellationTokenSource(); 203 | var cancellationToken = cts.Token; 204 | 205 | int numberOfCalls = 0; 206 | 207 | var results = await ParallelAsync.ForEachAsync(input, (el) => 208 | { 209 | Interlocked.Increment(ref numberOfCalls); 210 | 211 | return Task.FromResult(el); 212 | }, maxBatchSize: 1, cancellationToken: cancellationToken); 213 | 214 | Assert.Equal(10, numberOfCalls); 215 | Assert.Equal(numberOfCalls, results.Count()); 216 | 217 | cts.Dispose(); 218 | } 219 | 220 | [Fact] 221 | public async Task ParallelAsync_Throws_On_Invalid_Inputs() 222 | { 223 | var empty = new int[0].AsAsyncEnumerable(); 224 | IAsyncEnumerable nullEnumerable = null; 225 | 226 | var ex1 = await Assert.ThrowsAsync(() => ParallelAsync.ForEachAsync(nullEnumerable, (e) => Task.CompletedTask)); 227 | var ex2 = await Assert.ThrowsAsync(() => ParallelAsync.ForEachAsync(nullEnumerable, (e, ct) => Task.CompletedTask)); 228 | 229 | var ex3 = await Assert.ThrowsAsync(() => ParallelAsync.ForEachAsync(nullEnumerable, (e) => Task.FromResult(e))); 230 | var ex4 = await Assert.ThrowsAsync(() => ParallelAsync.ForEachAsync(nullEnumerable, (e, ct) => Task.FromResult(e))); 231 | 232 | var ex5 = await Assert.ThrowsAsync(() => ParallelAsync.ForEachAsync(empty, (Func)null)); 233 | var ex6 = await Assert.ThrowsAsync(() => ParallelAsync.ForEachAsync(empty, (Func)null)); 234 | 235 | var ex7 = await Assert.ThrowsAsync(() => ParallelAsync.ForEachAsync(empty, (Func>)null)); 236 | var ex8 = await Assert.ThrowsAsync(() => ParallelAsync.ForEachAsync(empty, (Func>)null)); 237 | 238 | var ex9 = await Assert.ThrowsAsync(() => ParallelAsync.ForEachAsync(empty, (e) => Task.CompletedTask, -1)); 239 | 240 | Assert.Equal("collection", ex1.ParamName); 241 | Assert.Equal("collection", ex2.ParamName); 242 | Assert.Equal("collection", ex3.ParamName); 243 | Assert.Equal("collection", ex4.ParamName); 244 | 245 | Assert.Equal("func", ex5.ParamName); 246 | Assert.Equal("func", ex6.ParamName); 247 | Assert.Equal("func", ex7.ParamName); 248 | Assert.Equal("func", ex8.ParamName); 249 | 250 | Assert.Equal("maxBatchSize", ex9.ParamName); 251 | } 252 | 253 | [Fact] 254 | public async Task ParallelAsync_Can_Batch_Basic_Work_Unordered() 255 | { 256 | const int numberOfElements = 100; 257 | var callCount = 0; 258 | var input = Enumerable.Range(1, numberOfElements).ToArray().AsAsyncEnumerable(); 259 | 260 | var results = await ParallelAsync.ForEachAsync(input, (el) => 261 | { 262 | var r = el + Interlocked.Increment(ref callCount); 263 | 264 | return Task.FromResult(r); 265 | }, maxBatchSize: 9, allowOutOfOrderProcessing: true, estimatedResultSize: numberOfElements); 266 | 267 | Assert.Equal(numberOfElements, callCount); 268 | Assert.Equal(numberOfElements, results.Count()); 269 | } 270 | 271 | [Fact] 272 | public async Task ParallelAsync_Can_Batch_Basic_Work_Void_Unordered() 273 | { 274 | const int numberOfElements = 100; 275 | var callCount = 0; 276 | var input = Enumerable.Range(1, numberOfElements).ToArray().AsAsyncEnumerable(); 277 | 278 | await ParallelAsync.ForEachAsync(input, (el) => 279 | { 280 | var r = el + Interlocked.Increment(ref callCount); 281 | 282 | return Task.CompletedTask; 283 | }, maxBatchSize: 9, allowOutOfOrderProcessing: true); 284 | 285 | Assert.Equal(numberOfElements, callCount); 286 | } 287 | 288 | [Fact] 289 | public async Task ParallelAsync_Can_Handle_Propagating_CancellationTokens_Unordered() 290 | { 291 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }.AsAsyncEnumerable(); 292 | 293 | var cts = new CancellationTokenSource(); 294 | var cancellationToken = cts.Token; 295 | 296 | int numberOfCalls = 0; 297 | 298 | cts.CancelAfter(250); 299 | 300 | await ParallelAsync.ForEachAsync(input, async (el) => 301 | { 302 | await Task.Delay(500); 303 | Interlocked.Increment(ref numberOfCalls); 304 | }, allowOutOfOrderProcessing: true, maxBatchSize: 4, cancellationToken: cancellationToken); 305 | 306 | Assert.True(numberOfCalls < 10); 307 | 308 | cts.Dispose(); 309 | } 310 | 311 | [Fact] 312 | public async Task ParallelAsync_TaskT_Can_Handle_Propagating_CancellationTokens_Unordered() 313 | { 314 | var input = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }.AsAsyncEnumerable(); 315 | 316 | var cts = new CancellationTokenSource(); 317 | var cancellationToken = cts.Token; 318 | 319 | int numberOfCalls = 0; 320 | 321 | cts.CancelAfter(250); 322 | 323 | var results = await ParallelAsync.ForEachAsync(input, async (el) => 324 | { 325 | await Task.Delay(500); 326 | 327 | Interlocked.Increment(ref numberOfCalls); 328 | return el; 329 | }, allowOutOfOrderProcessing: true, maxBatchSize: 4, cancellationToken: cancellationToken); 330 | 331 | Assert.True(numberOfCalls < 10); 332 | var numberOfResults = results.Count(); 333 | Assert.True(numberOfResults <= numberOfCalls, $"Expected less than, or equal to, {numberOfCalls}, but got {numberOfResults}"); 334 | 335 | cts.Dispose(); 336 | } 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /src/CSRakowski.Parallel/Extensions/ParallelAsyncEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace CSRakowski.Parallel.Extensions 9 | { 10 | /// 11 | /// Extension methods to allow using the functionalities of with a fluent syntax 12 | /// 13 | public static partial class ParallelAsyncEx 14 | { 15 | #region With.. Configuration methods 16 | 17 | /// 18 | /// Wraps the collection as an 19 | /// 20 | /// The element type 21 | /// The collection to wrap 22 | /// The wrapped collection 23 | /// Thrown when is null. 24 | public static IParallelAsyncEnumerable AsParallelAsync(this IEnumerable enumerable) 25 | { 26 | if (enumerable == null) 27 | { 28 | throw new ArgumentNullException(nameof(enumerable)); 29 | } 30 | 31 | if (enumerable is IParallelAsyncEnumerable parallelAsync) 32 | { 33 | return parallelAsync; 34 | } 35 | 36 | return new ParallelAsyncEnumerable(enumerable); 37 | } 38 | 39 | /// 40 | /// Wraps the collection as an 41 | /// 42 | /// The element type 43 | /// The collection to wrap 44 | /// The wrapped collection 45 | /// Thrown when is null. 46 | public static IParallelAsyncEnumerable AsParallelAsync(this IAsyncEnumerable enumerable) 47 | { 48 | if (enumerable == null) 49 | { 50 | throw new ArgumentNullException(nameof(enumerable)); 51 | } 52 | 53 | if (enumerable is IParallelAsyncEnumerable parallelAsync) 54 | { 55 | return parallelAsync; 56 | } 57 | 58 | return new ParallelAsyncEnumerable(enumerable); 59 | } 60 | 61 | /// 62 | /// Configure a maximum batch size to allow. 63 | /// 64 | /// The element type 65 | /// The 66 | /// The maximum batch size to allow. Use 0 to default to Environment.ProcessorCount 67 | /// The configured collections 68 | /// Thrown when is a negative number. 69 | public static IParallelAsyncEnumerable WithMaxDegreeOfParallelism(this IParallelAsyncEnumerable parallelAsync, int maxDegreeOfParallelism) 70 | { 71 | var obj = EnsureValidEnumerable(parallelAsync); 72 | 73 | if (maxDegreeOfParallelism < 0) 74 | { 75 | throw new ArgumentOutOfRangeException(nameof(maxDegreeOfParallelism), "Must be either 0 or a positive number"); 76 | } 77 | 78 | obj.MaxDegreeOfParallelism = maxDegreeOfParallelism; 79 | 80 | return obj; 81 | } 82 | 83 | /// 84 | /// Configured the estimated result size, 85 | /// 86 | /// The element type 87 | /// The 88 | /// The estimated size of the result collection. 89 | /// The configured collections 90 | /// 91 | /// The value is used to determine the size of the result to account for. 92 | /// The library will actually check if it can determine the size of , without actually consuming it, using the . 93 | /// If it is unable to determine a size there, it will fall back to the value you specified in . 94 | /// Setting this value to low, will mean a too small list will be allocated and you will have to pay a small performance hit for the resizing of the list during execution. 95 | /// 96 | /// Thrown when is a negative number. 97 | public static IParallelAsyncEnumerable WithEstimatedResultSize(this IParallelAsyncEnumerable parallelAsync, int estimatedResultSize) 98 | { 99 | var obj = EnsureValidEnumerable(parallelAsync); 100 | 101 | if (estimatedResultSize < 0) 102 | { 103 | throw new ArgumentOutOfRangeException(nameof(estimatedResultSize), "Must be either 0 or a positive number"); 104 | } 105 | 106 | obj.EstimatedResultSize = estimatedResultSize; 107 | 108 | return obj; 109 | } 110 | 111 | /// 112 | /// Configure whether or not to allow out of order processing. 113 | /// 114 | /// The element type 115 | /// The 116 | /// Boolean to allow out of order processing of input elements. 117 | /// The configured collections 118 | /// 119 | /// The flag allows you to specify wether to allow the out of order processing mode. 120 | /// This mode offers a performance improvement when the duration of each job varies, eg. due to network latency. 121 | /// When each run takes roughly the same amount of time, running in out of order mode can/will actually perform worse. 122 | /// As with all performance scenario's, do your own testing and pick what works for you. 123 | /// 124 | public static IParallelAsyncEnumerable WithOutOfOrderProcessing(this IParallelAsyncEnumerable parallelAsync, bool allowOutOfOrderProcessing) 125 | { 126 | var obj = EnsureValidEnumerable(parallelAsync); 127 | 128 | obj.AllowOutOfOrderProcessing = allowOutOfOrderProcessing; 129 | 130 | return obj; 131 | } 132 | 133 | #endregion With.. Configuration methods 134 | 135 | #region ForEachAsync overloads 136 | 137 | /// 138 | /// Runs the specified async method for each item of the input collection in a parallel/batched manner. 139 | /// 140 | /// The input item type 141 | /// The result item type 142 | /// The to process 143 | /// The async method to run for each item 144 | /// A 145 | /// The results of the operations 146 | /// Thrown when either or is null. 147 | /// Thrown when the configured maximum batch size is a negative number. 148 | public static Task> ForEachAsync(this IParallelAsyncEnumerable parallelAsync, Func> func, CancellationToken cancellationToken = default) 149 | { 150 | var obj = EnsureValidEnumerable(parallelAsync); 151 | 152 | if (obj.IsAsyncEnumerable) 153 | { 154 | return ParallelAsync.ForEachAsync(obj.AsyncEnumerable, func, obj.MaxDegreeOfParallelism, obj.AllowOutOfOrderProcessing, obj.EstimatedResultSize, cancellationToken); 155 | } 156 | else 157 | { 158 | return ParallelAsync.ForEachAsync(obj.Enumerable, func, obj.MaxDegreeOfParallelism, obj.AllowOutOfOrderProcessing, obj.EstimatedResultSize, cancellationToken); 159 | } 160 | } 161 | 162 | /// 163 | /// Runs the specified async method for each item of the input collection in a parallel/batched manner. 164 | /// 165 | /// The input item type 166 | /// The result item type 167 | /// The to process 168 | /// The async method to run for each item 169 | /// A 170 | /// The results of the operations 171 | /// Thrown when either or is null. 172 | /// Thrown when the configured maximum batch size is a negative number. 173 | public static Task> ForEachAsync(this IParallelAsyncEnumerable parallelAsync, Func> func, CancellationToken cancellationToken = default) 174 | { 175 | var obj = EnsureValidEnumerable(parallelAsync); 176 | 177 | if (obj.IsAsyncEnumerable) 178 | { 179 | return ParallelAsync.ForEachAsync(obj.AsyncEnumerable, func, obj.MaxDegreeOfParallelism, obj.AllowOutOfOrderProcessing, obj.EstimatedResultSize, cancellationToken); 180 | } 181 | else 182 | { 183 | return ParallelAsync.ForEachAsync(obj.Enumerable, func, obj.MaxDegreeOfParallelism, obj.AllowOutOfOrderProcessing, obj.EstimatedResultSize, cancellationToken); 184 | } 185 | } 186 | 187 | /// 188 | /// Runs the specified async method for each item of the input collection in a parallel/batched manner. 189 | /// 190 | /// The input item type 191 | /// The to process 192 | /// The async method to run for each item 193 | /// A 194 | /// The results of the operations 195 | /// Thrown when either or is null. 196 | /// Thrown when the configured maximum batch size is a negative number. 197 | public static Task ForEachAsync(this IParallelAsyncEnumerable parallelAsync, Func func, CancellationToken cancellationToken = default) 198 | { 199 | var obj = EnsureValidEnumerable(parallelAsync); 200 | 201 | if (obj.IsAsyncEnumerable) 202 | { 203 | return ParallelAsync.ForEachAsync(obj.AsyncEnumerable, func, obj.MaxDegreeOfParallelism, obj.AllowOutOfOrderProcessing, cancellationToken); 204 | } 205 | else 206 | { 207 | return ParallelAsync.ForEachAsync(obj.Enumerable, func, obj.MaxDegreeOfParallelism, obj.AllowOutOfOrderProcessing, cancellationToken); 208 | } 209 | } 210 | 211 | /// 212 | /// Runs the specified async method for each item of the input collection in a parallel/batched manner. 213 | /// 214 | /// The input item type 215 | /// The to process 216 | /// The async method to run for each item 217 | /// A 218 | /// The results of the operations 219 | /// Thrown when either or is null. 220 | /// Thrown when the configured maximum batch size is a negative number. 221 | public static Task ForEachAsync(this IParallelAsyncEnumerable parallelAsync, Func func, CancellationToken cancellationToken = default) 222 | { 223 | var obj = EnsureValidEnumerable(parallelAsync); 224 | 225 | if (obj.IsAsyncEnumerable) 226 | { 227 | return ParallelAsync.ForEachAsync(obj.AsyncEnumerable, func, obj.MaxDegreeOfParallelism, obj.AllowOutOfOrderProcessing, cancellationToken); 228 | } 229 | else 230 | { 231 | return ParallelAsync.ForEachAsync(obj.Enumerable, func, obj.MaxDegreeOfParallelism, obj.AllowOutOfOrderProcessing, cancellationToken); 232 | } 233 | } 234 | 235 | #endregion ForEachAsync overloads 236 | 237 | #region Internal Helpers 238 | 239 | /// 240 | /// Ensure that the passed in is a valid 241 | /// 242 | /// The element type 243 | /// The to check 244 | /// The 245 | /// Thrown when is not a valid 246 | private static ParallelAsyncEnumerable EnsureValidEnumerable(IParallelAsyncEnumerable parallelAsync) 247 | { 248 | var obj = parallelAsync as ParallelAsyncEnumerable; 249 | if (obj == null) 250 | { 251 | throw new ArgumentNullException(nameof(parallelAsync)); 252 | } 253 | return obj; 254 | } 255 | 256 | #endregion Internal Helpers 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/CSRakowski.Parallel/ParallelAsync.Unbatched.cs: -------------------------------------------------------------------------------- 1 | using CSRakowski.Parallel.Helpers; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace CSRakowski.Parallel 8 | { 9 | public static partial class ParallelAsync 10 | { 11 | #region IEnumerable 12 | 13 | /// 14 | /// Implementation to run the specified async method for each item of the input collection in an unbatched manner. 15 | /// 16 | /// The result item type 17 | /// The input item type 18 | /// The collection of items to use as input arguments 19 | /// The async method to run for each item 20 | /// The estimated size of the result collection. 21 | /// A 22 | /// The results of the operations 23 | private static Task> ForEachAsyncImplUnbatched(IEnumerable collection, Func> func, int estimatedResultSize, CancellationToken cancellationToken) 24 | { 25 | if (collection is TIn[] array) 26 | { 27 | return ForEachAsyncImplUnbatched_Array(array, func, cancellationToken); 28 | } 29 | else if (collection is IList list) 30 | { 31 | return ForEachAsyncImplUnbatched_IList(list, func, cancellationToken); 32 | } 33 | else 34 | { 35 | return ForEachAsyncImplUnbatched_IEnumerable(collection, func, estimatedResultSize, cancellationToken); 36 | } 37 | } 38 | 39 | /// 40 | /// Default implementation to run the specified async method for each item of the input in an unbatched manner. 41 | /// 42 | /// The result item type 43 | /// The input item type 44 | /// The of items to use as input arguments 45 | /// The async method to run for each item 46 | /// The estimated size of the result collection. 47 | /// A 48 | /// The results of the operations 49 | private static async Task> ForEachAsyncImplUnbatched_IEnumerable(IEnumerable collection, Func> func, int estimatedResultSize, CancellationToken cancellationToken) 50 | { 51 | var result = ListHelpers.GetList(collection, estimatedResultSize); 52 | 53 | long runId = ParallelAsyncEventSource.Log.GetRunId(); 54 | ParallelAsyncEventSource.Log.RunStart(runId, 1, false, estimatedResultSize); 55 | 56 | int batchId = 0; 57 | foreach (var element in collection) 58 | { 59 | if (cancellationToken.IsCancellationRequested) 60 | { 61 | break; 62 | } 63 | 64 | ParallelAsyncEventSource.Log.BatchStart(runId, batchId, 1); 65 | 66 | var resultElement = await func(element, cancellationToken).ConfigureAwait(false); 67 | result.Add(resultElement); 68 | 69 | ParallelAsyncEventSource.Log.BatchStop(runId, batchId); 70 | 71 | batchId++; 72 | } 73 | 74 | ParallelAsyncEventSource.Log.RunStop(runId); 75 | 76 | return result; 77 | } 78 | 79 | /// 80 | /// Special case implementation to run the specified async method for each item of the input in an unbatched manner. 81 | /// 82 | /// The result item type 83 | /// The input item type 84 | /// The of items to use as input arguments 85 | /// The async method to run for each item 86 | /// A 87 | /// The results of the operations 88 | private static async Task> ForEachAsyncImplUnbatched_IList(IList collection, Func> func, CancellationToken cancellationToken) 89 | { 90 | var collectionCount = collection.Count; 91 | 92 | var result = ListHelpers.GetList(collectionCount); 93 | 94 | long runId = ParallelAsyncEventSource.Log.GetRunId(); 95 | ParallelAsyncEventSource.Log.RunStart(runId, 1, false, 0); 96 | 97 | for (int batchId = 0; batchId < collectionCount; batchId++) 98 | { 99 | if (cancellationToken.IsCancellationRequested) 100 | { 101 | break; 102 | } 103 | 104 | ParallelAsyncEventSource.Log.BatchStart(runId, batchId, 1); 105 | 106 | var element = collection[batchId]; 107 | var resultElement = await func(element, cancellationToken).ConfigureAwait(false); 108 | result.Add(resultElement); 109 | 110 | ParallelAsyncEventSource.Log.BatchStop(runId, batchId); 111 | } 112 | 113 | ParallelAsyncEventSource.Log.RunStop(runId); 114 | 115 | return result; 116 | } 117 | 118 | /// 119 | /// Special case implementation to run the specified async method for each item of the input T[] in an unbatched manner. 120 | /// 121 | /// The result item type 122 | /// The input item type 123 | /// The T[] of items to use as input arguments 124 | /// The async method to run for each item 125 | /// A 126 | /// The results of the operations 127 | private static async Task> ForEachAsyncImplUnbatched_Array(TIn[] collection, Func> func, CancellationToken cancellationToken) 128 | { 129 | var result = ListHelpers.GetList(collection.Length); 130 | 131 | long runId = ParallelAsyncEventSource.Log.GetRunId(); 132 | ParallelAsyncEventSource.Log.RunStart(runId, 1, false, 0); 133 | 134 | for (int batchId = 0; batchId < collection.Length; batchId++) 135 | { 136 | if (cancellationToken.IsCancellationRequested) 137 | { 138 | break; 139 | } 140 | 141 | ParallelAsyncEventSource.Log.BatchStart(runId, batchId, 1); 142 | 143 | var element = collection[batchId]; 144 | var resultElement = await func(element, cancellationToken).ConfigureAwait(false); 145 | result.Add(resultElement); 146 | 147 | ParallelAsyncEventSource.Log.BatchStop(runId, batchId); 148 | } 149 | 150 | ParallelAsyncEventSource.Log.RunStop(runId); 151 | 152 | return result; 153 | } 154 | 155 | /// 156 | /// Implementation to run the specified async method for each item of the input collection in an unbatched manner. 157 | /// 158 | /// The input item type 159 | /// The collection of items to use as input arguments 160 | /// The async method to run for each item 161 | /// A 162 | /// A signaling completion 163 | private static Task ForEachAsyncImplUnbatched(IEnumerable collection, Func func, CancellationToken cancellationToken) 164 | { 165 | if (collection is TIn[] array) 166 | { 167 | return ForEachAsyncImplUnbatched_Array(array, func, cancellationToken); 168 | } 169 | else if (collection is IList list) 170 | { 171 | return ForEachAsyncImplUnbatched_IList(list, func, cancellationToken); 172 | } 173 | else 174 | { 175 | return ForEachAsyncImplUnbatched_IEnumerable(collection, func, cancellationToken); 176 | } 177 | } 178 | 179 | /// 180 | /// Default implementation to run the specified async method for each item of the input in an unbatched manner. 181 | /// 182 | /// The input item type 183 | /// The of items to use as input arguments 184 | /// The async method to run for each item 185 | /// A 186 | /// A signaling completion 187 | private static async Task ForEachAsyncImplUnbatched_IEnumerable(IEnumerable collection, Func func, CancellationToken cancellationToken) 188 | { 189 | long runId = ParallelAsyncEventSource.Log.GetRunId(); 190 | ParallelAsyncEventSource.Log.RunStart(runId, 1, false, 0); 191 | 192 | int batchId = 0; 193 | foreach (var element in collection) 194 | { 195 | if (cancellationToken.IsCancellationRequested) 196 | { 197 | break; 198 | } 199 | 200 | ParallelAsyncEventSource.Log.BatchStart(runId, batchId, 1); 201 | 202 | await func(element, cancellationToken).ConfigureAwait(false); 203 | 204 | ParallelAsyncEventSource.Log.BatchStop(runId, batchId); 205 | 206 | batchId++; 207 | } 208 | 209 | ParallelAsyncEventSource.Log.RunStop(runId); 210 | } 211 | 212 | /// 213 | /// Special case implementation to run the specified async method for each item of the input in an unbatched manner. 214 | /// 215 | /// The input item type 216 | /// The of items to use as input arguments 217 | /// The async method to run for each item 218 | /// A 219 | /// A signaling completion 220 | private static async Task ForEachAsyncImplUnbatched_IList(IList collection, Func func, CancellationToken cancellationToken) 221 | { 222 | long runId = ParallelAsyncEventSource.Log.GetRunId(); 223 | ParallelAsyncEventSource.Log.RunStart(runId, 1, false, 0); 224 | 225 | var collectionCount = collection.Count; 226 | for (int batchId = 0; batchId < collectionCount; batchId++) 227 | { 228 | if (cancellationToken.IsCancellationRequested) 229 | { 230 | break; 231 | } 232 | 233 | ParallelAsyncEventSource.Log.BatchStart(runId, batchId, 1); 234 | 235 | var element = collection[batchId]; 236 | await func(element, cancellationToken).ConfigureAwait(false); 237 | 238 | ParallelAsyncEventSource.Log.BatchStop(runId, batchId); 239 | } 240 | 241 | ParallelAsyncEventSource.Log.RunStop(runId); 242 | } 243 | 244 | /// 245 | /// Special case implementation to run the specified async method for each item of the input T[] in an unbatched manner. 246 | /// 247 | /// The input item type 248 | /// The T[] of items to use as input arguments 249 | /// The async method to run for each item 250 | /// A 251 | /// A signaling completion 252 | private static async Task ForEachAsyncImplUnbatched_Array(TIn[] collection, Func func, CancellationToken cancellationToken) 253 | { 254 | long runId = ParallelAsyncEventSource.Log.GetRunId(); 255 | ParallelAsyncEventSource.Log.RunStart(runId, 1, false, 0); 256 | 257 | for (int batchId = 0; batchId < collection.Length; batchId++) 258 | { 259 | if (cancellationToken.IsCancellationRequested) 260 | { 261 | break; 262 | } 263 | 264 | ParallelAsyncEventSource.Log.BatchStart(runId, batchId, 1); 265 | 266 | var element = collection[batchId]; 267 | await func(element, cancellationToken).ConfigureAwait(false); 268 | 269 | ParallelAsyncEventSource.Log.BatchStop(runId, batchId); 270 | } 271 | 272 | ParallelAsyncEventSource.Log.RunStop(runId); 273 | } 274 | 275 | #endregion IEnumerable 276 | 277 | #region IAsyncEnumerable 278 | 279 | private static async Task> ForEachAsyncImplUnbatched(IAsyncEnumerable collection, Func> func, int estimatedResultSize, CancellationToken cancellationToken) 280 | { 281 | var result = ListHelpers.GetList(estimatedResultSize); 282 | 283 | long runId = ParallelAsyncEventSource.Log.GetRunId(); 284 | ParallelAsyncEventSource.Log.RunStart(runId, 1, false, estimatedResultSize); 285 | 286 | var enumerator = collection.GetAsyncEnumerator(cancellationToken); 287 | try 288 | { 289 | var hasNext = true; 290 | int batchId = 0; 291 | 292 | while (!cancellationToken.IsCancellationRequested) 293 | { 294 | hasNext = await enumerator.MoveNextAsync().ConfigureAwait(false); 295 | 296 | if (!hasNext) 297 | { 298 | break; 299 | } 300 | 301 | ParallelAsyncEventSource.Log.BatchStart(runId, batchId, 1); 302 | 303 | var element = enumerator.Current; 304 | var resultElement = await func(element, cancellationToken).ConfigureAwait(false); 305 | result.Add(resultElement); 306 | 307 | ParallelAsyncEventSource.Log.BatchStop(runId, batchId); 308 | 309 | batchId++; 310 | } 311 | } 312 | finally 313 | { 314 | await enumerator.DisposeAsync().ConfigureAwait(false); 315 | } 316 | 317 | ParallelAsyncEventSource.Log.RunStop(runId); 318 | 319 | return result; 320 | } 321 | 322 | private static async Task ForEachAsyncImplUnbatched(IAsyncEnumerable collection, Func func, CancellationToken cancellationToken) 323 | { 324 | long runId = ParallelAsyncEventSource.Log.GetRunId(); 325 | ParallelAsyncEventSource.Log.RunStart(runId, 1, false, 0); 326 | 327 | var enumerator = collection.GetAsyncEnumerator(cancellationToken); 328 | try 329 | { 330 | var hasNext = true; 331 | int batchId = 0; 332 | 333 | while (!cancellationToken.IsCancellationRequested) 334 | { 335 | hasNext = await enumerator.MoveNextAsync().ConfigureAwait(false); 336 | 337 | if (!hasNext) 338 | { 339 | break; 340 | } 341 | 342 | ParallelAsyncEventSource.Log.BatchStart(runId, batchId, 1); 343 | 344 | var element = enumerator.Current; 345 | await func(element, cancellationToken).ConfigureAwait(false); 346 | 347 | ParallelAsyncEventSource.Log.BatchStop(runId, batchId); 348 | 349 | batchId++; 350 | } 351 | } 352 | finally 353 | { 354 | await enumerator.DisposeAsync().ConfigureAwait(false); 355 | } 356 | 357 | ParallelAsyncEventSource.Log.RunStop(runId); 358 | } 359 | 360 | #endregion IAsyncEnumerable 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/CSRakowski.Parallel/ParallelAsync.Ordered.cs: -------------------------------------------------------------------------------- 1 | using CSRakowski.Parallel.Helpers; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace CSRakowski.Parallel 8 | { 9 | public static partial class ParallelAsync 10 | { 11 | #region IEnumerable 12 | 13 | /// 14 | /// Implementation to run the specified async method for each item of the input collection in an batched manner, whilst preserving ordering as much as possible. 15 | /// 16 | /// The result item type 17 | /// The input item type 18 | /// The collection of items to use as input arguments 19 | /// The async method to run for each item 20 | /// The batch size to use 21 | /// The estimated size of the result collection. 22 | /// A 23 | /// The results of the operations 24 | private static Task> ForEachAsyncImplOrdered(IEnumerable collection, Func> func, int batchSize, int estimatedResultSize, CancellationToken cancellationToken) 25 | { 26 | if (collection is TIn[] array) 27 | { 28 | return ForEachAsyncImplOrdered_Array(array, func, batchSize, cancellationToken); 29 | } 30 | else 31 | { 32 | return ForEachAsyncImplOrdered_IEnumerable(collection, func, batchSize, estimatedResultSize, cancellationToken); 33 | } 34 | } 35 | 36 | /// 37 | /// Default implementation to run the specified async method for each item of the input in an batched manner, whilst preserving ordering as much as possible. 38 | /// 39 | /// The result item type 40 | /// The input item type 41 | /// The of items to use as input arguments 42 | /// The async method to run for each item 43 | /// The batch size to use 44 | /// The estimated size of the result collection. 45 | /// A 46 | /// The results of the operations 47 | private static async Task> ForEachAsyncImplOrdered_IEnumerable(IEnumerable collection, Func> func, int batchSize, int estimatedResultSize, CancellationToken cancellationToken) 48 | { 49 | // Using arrays is only marginally faster (in the best case, when the determined/estimated result size is correct) 50 | // In cases where the resultsize is off, we inch closer to the speed offered by List (for obvious reasons) 51 | // To prevent duplicating code, we are not going to be directly using arrays for the results collection here. 52 | var result = ListHelpers.GetList(collection, estimatedResultSize); 53 | 54 | long runId = ParallelAsyncEventSource.Log.GetRunId(); 55 | ParallelAsyncEventSource.Log.RunStart(runId, batchSize, false, estimatedResultSize); 56 | 57 | using (var enumerator = collection.GetEnumerator()) 58 | { 59 | var hasNext = true; 60 | int batchId = 0; 61 | 62 | while (hasNext && !cancellationToken.IsCancellationRequested) 63 | { 64 | var taskList = new Task[batchSize]; 65 | 66 | int threadIndex; 67 | for (threadIndex = 0; threadIndex < batchSize; threadIndex++) 68 | { 69 | if (cancellationToken.IsCancellationRequested) 70 | { 71 | break; 72 | } 73 | 74 | hasNext = enumerator.MoveNext(); 75 | 76 | if (!hasNext) 77 | { 78 | break; 79 | } 80 | 81 | var element = enumerator.Current; 82 | 83 | var task = func(element, cancellationToken); 84 | taskList[threadIndex] = task; 85 | } 86 | 87 | // If we reach the end, we need to ensure there are no NULLs in the taskList as Task.WhenAll breaks on those. 88 | if (threadIndex < batchSize) 89 | { 90 | var temp = new Task[threadIndex]; 91 | Array.Copy(taskList, temp, threadIndex); 92 | taskList = temp; 93 | } 94 | 95 | ParallelAsyncEventSource.Log.BatchStart(runId, batchId, taskList.Length); 96 | 97 | var batchResults = await Task.WhenAll(taskList).ConfigureAwait(false); 98 | 99 | result.AddRange(batchResults); 100 | 101 | ParallelAsyncEventSource.Log.BatchStop(runId, batchId); 102 | 103 | batchId++; 104 | } 105 | } 106 | 107 | ParallelAsyncEventSource.Log.RunStop(runId); 108 | 109 | return result; 110 | } 111 | 112 | /// 113 | /// Special case implementation to run the specified async method for each item of the input T[] in an batched manner, whilst preserving ordering as much as possible. 114 | /// 115 | /// The result item type 116 | /// The input item type 117 | /// The T[] of items to use as input arguments 118 | /// The async method to run for each item 119 | /// The batch size to use 120 | /// A 121 | /// The results of the operations 122 | private static async Task> ForEachAsyncImplOrdered_Array(TIn[] collection, Func> func, int batchSize, CancellationToken cancellationToken) 123 | { 124 | var result = ListHelpers.GetList(collection.Length); 125 | 126 | long runId = ParallelAsyncEventSource.Log.GetRunId(); 127 | ParallelAsyncEventSource.Log.RunStart(runId, batchSize, false, collection.Length); 128 | 129 | int batchId = 0; 130 | int collectionIndex = 0; 131 | 132 | while (collectionIndex < collection.Length && !cancellationToken.IsCancellationRequested) 133 | { 134 | var taskList = new Task[batchSize]; 135 | 136 | int threadIndex; 137 | for (threadIndex = 0; threadIndex < batchSize && collectionIndex < collection.Length; threadIndex++) 138 | { 139 | if (cancellationToken.IsCancellationRequested) 140 | { 141 | break; 142 | } 143 | 144 | var element = collection[collectionIndex]; 145 | 146 | var task = func(element, cancellationToken); 147 | taskList[threadIndex] = task; 148 | 149 | collectionIndex++; 150 | } 151 | 152 | // If we reach the end, we need to ensure there are no NULLs in the taskList as Task.WhenAll breaks on those. 153 | if (threadIndex < batchSize) 154 | { 155 | var temp = new Task[threadIndex]; 156 | Array.Copy(taskList, temp, threadIndex); 157 | taskList = temp; 158 | } 159 | 160 | ParallelAsyncEventSource.Log.BatchStart(runId, batchId, taskList.Length); 161 | 162 | var batchResults = await Task.WhenAll(taskList).ConfigureAwait(false); 163 | 164 | result.AddRange(batchResults); 165 | 166 | ParallelAsyncEventSource.Log.BatchStop(runId, batchId); 167 | 168 | batchId++; 169 | } 170 | 171 | ParallelAsyncEventSource.Log.RunStop(runId); 172 | 173 | return result; 174 | } 175 | 176 | /// 177 | /// Implementation to run the specified async method for each item of the input collection in an batched manner, whilst preserving ordering as much as possible. 178 | /// 179 | /// The input item type 180 | /// The collection of items to use as input arguments 181 | /// The async method to run for each item 182 | /// The batch size to use 183 | /// A 184 | /// A signaling completion 185 | private static Task ForEachAsyncImplOrdered(IEnumerable collection, Func func, int batchSize, CancellationToken cancellationToken) 186 | { 187 | if (collection is TIn[] array) 188 | { 189 | return ForEachAsyncImplOrdered_Array(array, func, batchSize, cancellationToken); 190 | } 191 | else 192 | { 193 | return ForEachAsyncImplOrdered_IEnumerable(collection, func, batchSize, cancellationToken); 194 | } 195 | } 196 | 197 | /// 198 | /// Default implementation to run the specified async method for each item of the input in an batched manner, whilst preserving ordering as much as possible. 199 | /// 200 | /// The input item type 201 | /// The of items to use as input arguments 202 | /// The async method to run for each item 203 | /// The batch size to use 204 | /// A 205 | /// A signaling completion 206 | private static async Task ForEachAsyncImplOrdered_IEnumerable(IEnumerable collection, Func func, int batchSize, CancellationToken cancellationToken) 207 | { 208 | long runId = ParallelAsyncEventSource.Log.GetRunId(); 209 | ParallelAsyncEventSource.Log.RunStart(runId, batchSize, false, 0); 210 | 211 | using (var enumerator = collection.GetEnumerator()) 212 | { 213 | var hasNext = true; 214 | int batchId = 0; 215 | 216 | while (hasNext && !cancellationToken.IsCancellationRequested) 217 | { 218 | var taskList = new Task[batchSize]; 219 | 220 | int threadIndex; 221 | for (threadIndex = 0; threadIndex < batchSize; threadIndex++) 222 | { 223 | if (cancellationToken.IsCancellationRequested) 224 | { 225 | break; 226 | } 227 | 228 | hasNext = enumerator.MoveNext(); 229 | 230 | if (!hasNext) 231 | { 232 | break; 233 | } 234 | 235 | var element = enumerator.Current; 236 | 237 | var task = func(element, cancellationToken); 238 | taskList[threadIndex] = task; 239 | } 240 | 241 | // If we reach the end, we need to ensure there are no NULLs in the taskList as Task.WhenAll breaks on those. 242 | if (threadIndex < batchSize) 243 | { 244 | var temp = new Task[threadIndex]; 245 | Array.Copy(taskList, temp, threadIndex); 246 | taskList = temp; 247 | } 248 | 249 | ParallelAsyncEventSource.Log.BatchStart(runId, batchId, taskList.Length); 250 | 251 | await Task.WhenAll(taskList).ConfigureAwait(false); 252 | 253 | ParallelAsyncEventSource.Log.BatchStop(runId, batchId); 254 | 255 | batchId++; 256 | } 257 | } 258 | 259 | ParallelAsyncEventSource.Log.RunStop(runId); 260 | } 261 | 262 | /// 263 | /// Special case implementation to run the specified async method for each item of the input T[] in an batched manner, whilst preserving ordering as much as possible. 264 | /// 265 | /// The input item type 266 | /// The T[] of items to use as input arguments 267 | /// The async method to run for each item 268 | /// The batch size to use 269 | /// A 270 | /// A signaling completion 271 | private static async Task ForEachAsyncImplOrdered_Array(TIn[] collection, Func func, int batchSize, CancellationToken cancellationToken) 272 | { 273 | long runId = ParallelAsyncEventSource.Log.GetRunId(); 274 | ParallelAsyncEventSource.Log.RunStart(runId, batchSize, false, collection.Length); 275 | 276 | int batchId = 0; 277 | int collectionIndex = 0; 278 | 279 | while (collectionIndex < collection.Length && !cancellationToken.IsCancellationRequested) 280 | { 281 | var taskList = new Task[batchSize]; 282 | 283 | int threadIndex; 284 | for (threadIndex = 0; threadIndex < batchSize && collectionIndex < collection.Length; threadIndex++) 285 | { 286 | if (cancellationToken.IsCancellationRequested) 287 | { 288 | break; 289 | } 290 | 291 | var element = collection[collectionIndex]; 292 | 293 | var task = func(element, cancellationToken); 294 | taskList[threadIndex] = task; 295 | 296 | collectionIndex++; 297 | } 298 | 299 | // If we reach the end, we need to ensure there are no NULLs in the taskList as Task.WhenAll breaks on those. 300 | if (threadIndex < batchSize) 301 | { 302 | var temp = new Task[threadIndex]; 303 | Array.Copy(taskList, temp, threadIndex); 304 | taskList = temp; 305 | } 306 | 307 | ParallelAsyncEventSource.Log.BatchStart(runId, batchId, taskList.Length); 308 | 309 | await Task.WhenAll(taskList).ConfigureAwait(false); 310 | 311 | ParallelAsyncEventSource.Log.BatchStop(runId, batchId); 312 | 313 | batchId++; 314 | } 315 | 316 | ParallelAsyncEventSource.Log.RunStop(runId); 317 | } 318 | 319 | #endregion IEnumerable 320 | 321 | #region IAsyncEnumerable 322 | 323 | private static async Task> ForEachAsyncImplOrdered(IAsyncEnumerable collection, Func> func, int batchSize, int estimatedResultSize, CancellationToken cancellationToken) 324 | { 325 | var result = ListHelpers.GetList(estimatedResultSize); 326 | 327 | long runId = ParallelAsyncEventSource.Log.GetRunId(); 328 | ParallelAsyncEventSource.Log.RunStart(runId, batchSize, false, estimatedResultSize); 329 | 330 | var enumerator = collection.GetAsyncEnumerator(cancellationToken); 331 | try 332 | { 333 | var hasNext = true; 334 | int batchId = 0; 335 | 336 | while (hasNext && !cancellationToken.IsCancellationRequested) 337 | { 338 | var taskList = new Task[batchSize]; 339 | 340 | int threadIndex; 341 | for (threadIndex = 0; threadIndex < batchSize; threadIndex++) 342 | { 343 | if (cancellationToken.IsCancellationRequested) 344 | { 345 | break; 346 | } 347 | 348 | hasNext = await enumerator.MoveNextAsync().ConfigureAwait(false); 349 | 350 | if (!hasNext) 351 | { 352 | break; 353 | } 354 | 355 | var element = enumerator.Current; 356 | 357 | var task = func(element, cancellationToken); 358 | taskList[threadIndex] = task; 359 | } 360 | 361 | // If we reach the end, we need to ensure there are no NULLs in the taskList as Task.WhenAll breaks on those. 362 | if (threadIndex < batchSize) 363 | { 364 | var temp = new Task[threadIndex]; 365 | Array.Copy(taskList, temp, threadIndex); 366 | taskList = temp; 367 | } 368 | 369 | ParallelAsyncEventSource.Log.BatchStart(runId, batchId, taskList.Length); 370 | 371 | var batchResults = await Task.WhenAll(taskList).ConfigureAwait(false); 372 | result.AddRange(batchResults); 373 | 374 | ParallelAsyncEventSource.Log.BatchStop(runId, batchId); 375 | 376 | batchId++; 377 | } 378 | } 379 | finally 380 | { 381 | await enumerator.DisposeAsync().ConfigureAwait(false); 382 | } 383 | 384 | ParallelAsyncEventSource.Log.RunStop(runId); 385 | 386 | return result; 387 | } 388 | 389 | private static async Task ForEachAsyncImplOrdered(IAsyncEnumerable collection, Func func, int batchSize, CancellationToken cancellationToken) 390 | { 391 | long runId = ParallelAsyncEventSource.Log.GetRunId(); 392 | ParallelAsyncEventSource.Log.RunStart(runId, batchSize, false, 0); 393 | 394 | var enumerator = collection.GetAsyncEnumerator(cancellationToken); 395 | try 396 | { 397 | var hasNext = true; 398 | int batchId = 0; 399 | 400 | while (hasNext && !cancellationToken.IsCancellationRequested) 401 | { 402 | var taskList = new Task[batchSize]; 403 | 404 | int threadIndex; 405 | for (threadIndex = 0; threadIndex < batchSize; threadIndex++) 406 | { 407 | if (cancellationToken.IsCancellationRequested) 408 | { 409 | break; 410 | } 411 | 412 | hasNext = await enumerator.MoveNextAsync().ConfigureAwait(false); 413 | 414 | if (!hasNext) 415 | { 416 | break; 417 | } 418 | 419 | var element = enumerator.Current; 420 | 421 | var task = func(element, cancellationToken); 422 | taskList[threadIndex] = task; 423 | } 424 | 425 | // If we reach the end, we need to ensure there are no NULLs in the taskList as Task.WhenAll breaks on those. 426 | if (threadIndex < batchSize) 427 | { 428 | var temp = new Task[threadIndex]; 429 | Array.Copy(taskList, temp, threadIndex); 430 | taskList = temp; 431 | } 432 | 433 | ParallelAsyncEventSource.Log.BatchStart(runId, batchId, taskList.Length); 434 | 435 | await Task.WhenAll(taskList).ConfigureAwait(false); 436 | 437 | ParallelAsyncEventSource.Log.BatchStop(runId, batchId); 438 | 439 | batchId++; 440 | } 441 | } 442 | finally 443 | { 444 | await enumerator.DisposeAsync().ConfigureAwait(false); 445 | } 446 | 447 | ParallelAsyncEventSource.Log.RunStop(runId); 448 | } 449 | 450 | #endregion IAsyncEnumerable 451 | } 452 | } 453 | --------------------------------------------------------------------------------