├── .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: [](https://www.nuget.org/packages/CSRakowski.ParallelAsync/)
5 | and GitHub: [](https://github.com/csrakowski/ParallelAsync/)
6 |
7 | On the side, working on improving usage of [](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