Skip to content

Commit ff0c5b9

Browse files
committed
New ConcurrentForEach library
1 parent 667b2f1 commit ff0c5b9

File tree

9 files changed

+395
-0
lines changed

9 files changed

+395
-0
lines changed

Source/Redgate.MicroLibraries.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ULibs.FullExceptionString",
1515
EndProject
1616
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ULibs.TinyJsonDeser", "ULibs.TinyJsonDeser\ULibs.TinyJsonDeser.csproj", "{F46791E7-5434-400B-B5B4-6659909C262E}"
1717
EndProject
18+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ULibs.ConcurrentForEach", "ULibs.ConcurrentForEach\ULibs.ConcurrentForEach.csproj", "{B9950135-5A39-4B4C-8A61-D6105CA03396}"
19+
EndProject
1820
Global
1921
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2022
Debug|Any CPU = Debug|Any CPU
@@ -45,6 +47,10 @@ Global
4547
{F46791E7-5434-400B-B5B4-6659909C262E}.Debug|Any CPU.Build.0 = Debug|Any CPU
4648
{F46791E7-5434-400B-B5B4-6659909C262E}.Release|Any CPU.ActiveCfg = Release|Any CPU
4749
{F46791E7-5434-400B-B5B4-6659909C262E}.Release|Any CPU.Build.0 = Release|Any CPU
50+
{B9950135-5A39-4B4C-8A61-D6105CA03396}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
51+
{B9950135-5A39-4B4C-8A61-D6105CA03396}.Debug|Any CPU.Build.0 = Debug|Any CPU
52+
{B9950135-5A39-4B4C-8A61-D6105CA03396}.Release|Any CPU.ActiveCfg = Release|Any CPU
53+
{B9950135-5A39-4B4C-8A61-D6105CA03396}.Release|Any CPU.Build.0 = Release|Any CPU
4854
EndGlobalSection
4955
GlobalSection(SolutionProperties) = preSolution
5056
HideSolutionNode = FALSE
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using System.Runtime.CompilerServices;
2+
3+
[assembly: InternalsVisibleTo("Ulibs.Tests")]
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
using System;
2+
using System.Collections.Generic;
3+
/***using System.Diagnostics.CodeAnalysis;***/
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
7+
namespace /***$rootnamespace$.***/ULibs.ConcurrentForEach
8+
{
9+
/***[ExcludeFromCodeCoverage]***/
10+
internal static class Concurrent
11+
{
12+
/// <summary>
13+
/// Applies an asynchronous operation to each element in a sequence.
14+
/// </summary>
15+
/// <typeparam name="T">The type of elements in the sequence.</typeparam>
16+
/// <param name="source">The input sequence of elements.</param>
17+
/// <param name="func">The asynchronous operation applied to each element in the sequence. It's strongly
18+
/// recommended that the operation should take care to handle its own expected exceptions.</param>
19+
/// <param name="maxConcurrentTasks">
20+
/// <para>
21+
/// The maximum number of concurrent operations. Must be greater than or equal to 1.
22+
/// </para>
23+
/// <para>
24+
/// The number of simultaneous operations can vary depending on the use case. For cpu intensive
25+
/// operations, consider using <see cref="Environment.ProcessorCount">Environment.ProcessorCount</see>.
26+
/// For operations that invoke the same web service for each item, RFC 7230 suggests that the number
27+
/// of simultaneous requests/connections should be limited (https://tools.ietf.org/html/rfc7230#section-6.4).
28+
/// A search for the connection limits used by common web-browsers suggests that a value in the range 6-8 is
29+
/// appropriate (any more, and you risk triggering abuse detection mechanisms). For operations that invoke a
30+
/// different web service for each item, a search for the connection limits used by common web-browsers
31+
/// suggests that a value in the range 10-20 is appropriate.
32+
/// </para>
33+
/// </param>
34+
/// <param name="cancellationToken">Used to cancel the operations.</param>
35+
/// <returns>A task that can be awaited upon for all operations to complete. Awaiting on the task will
36+
/// raise an <see cref="AggregateException"/> if any operation fails, or work is cancelled via the
37+
/// <paramref name="cancellationToken"/>.</returns>
38+
public static Task ForEachAsync<T>(
39+
this IEnumerable<T> source,
40+
Func<T, Task> func,
41+
int maxConcurrentTasks,
42+
CancellationToken cancellationToken)
43+
{
44+
if (func == null) throw new ArgumentNullException(nameof(func));
45+
return source.ForEachAsync((item, _) => func(item), maxConcurrentTasks, cancellationToken);
46+
}
47+
48+
/// <summary>
49+
/// Applies an asynchronous operation to each element in a sequence.
50+
/// </summary>
51+
/// <typeparam name="T">The type of elements in the sequence.</typeparam>
52+
/// <param name="source">The input sequence of elements.</param>
53+
/// <param name="func">The asynchronous operation applied to each element in the sequence. It's strongly
54+
/// recommended that the operation should take care to handle its own expected exceptions.</param>
55+
/// <param name="maxConcurrentTasks">
56+
/// <para>
57+
/// The maximum number of concurrent operations. Must be greater than or equal to 1.
58+
/// </para>
59+
/// <para>
60+
/// The number of simultaneous operations can vary depending on the use case. For cpu intensive
61+
/// operations, consider using <see cref="Environment.ProcessorCount">Environment.ProcessorCount</see>.
62+
/// For operations that invoke the same web service for each item, RFC 7230 suggests that the number
63+
/// of simultaneous requests/connections should be limited (https://tools.ietf.org/html/rfc7230#section-6.4).
64+
/// A search for the connection limits used by common web-browsers suggests that a value in the range 6-8 is
65+
/// appropriate (any more, and you risk triggering abuse detection mechanisms). For operations that invoke a
66+
/// different web service for each item, a search for the connection limits used by common web-browsers
67+
/// suggests that a value in the range 10-20 is appropriate.
68+
/// </para>
69+
/// </param>
70+
/// <param name="cancellationToken">Used to cancel the operations.</param>
71+
/// <returns>A task that can be awaited upon for all operations to complete. Awaiting on the task will
72+
/// raise an <see cref="AggregateException"/> if any operation fails, or work is cancelled via the
73+
/// <paramref name="cancellationToken"/>.</returns>
74+
public static async Task ForEachAsync<T>(
75+
this IEnumerable<T> source,
76+
Func<T, CancellationToken, Task> func,
77+
int maxConcurrentTasks,
78+
CancellationToken cancellationToken)
79+
{
80+
if (maxConcurrentTasks < 1)
81+
throw new ArgumentException("Value cannot be less than 1", nameof(maxConcurrentTasks));
82+
if (source == null) throw new ArgumentNullException(nameof(source));
83+
if (func == null) throw new ArgumentNullException(nameof(func));
84+
85+
using (var semaphore = new SemaphoreSlim(maxConcurrentTasks, maxConcurrentTasks))
86+
{
87+
var tasks = new List<Task>();
88+
foreach (var item in source)
89+
{
90+
// Wait for the next available slot.
91+
try
92+
{
93+
await semaphore.WaitAsync(cancellationToken);
94+
}
95+
catch (OperationCanceledException exception)
96+
{
97+
tasks.Add(Task.FromException(exception));
98+
break;
99+
}
100+
101+
// Discard completed tasks. Not strictly necessary, but keeps the list size down.
102+
tasks.RemoveAll(task => task.IsCompleted);
103+
104+
// Kick-off the next task.
105+
tasks.Add(CreateTask(func, item, cancellationToken).ReleaseSemaphoreOnCompletion(semaphore));
106+
}
107+
108+
await Task.WhenAll(tasks);
109+
}
110+
}
111+
112+
private static Task CreateTask<T>(
113+
Func<T, CancellationToken, Task> func, T item, CancellationToken cancellationToken)
114+
{
115+
try
116+
{
117+
return func(item, cancellationToken);
118+
}
119+
catch (Exception exception)
120+
{
121+
return Task.FromException(exception);
122+
}
123+
}
124+
125+
private static async Task ReleaseSemaphoreOnCompletion(this Task task, SemaphoreSlim semaphore)
126+
{
127+
try
128+
{
129+
await task;
130+
}
131+
finally
132+
{
133+
semaphore.Release();
134+
}
135+
}
136+
}
137+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Provides a way to asynchronously apply an operation to each element in a sequence, whilst limiting the maximum number of concurrent operations.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# ULibs.ConcurrentForEach release notes
2+
3+
## 1.0.0
4+
5+
### Features
6+
7+
- New `ForEachAsync` extension method for `IEnumerable<T>` that applies an asynchronous operation to each element, whilst limiting the maximum number of concurrent operations.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<Copyright>2018</Copyright>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="Microsoft.DotNet.Analyzers.Compatibility" Version="0.2.12-alpha" />
10+
</ItemGroup>
11+
12+
</Project>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0"?>
2+
<!-- http://docs.nuget.org/docs/reference/nuspec-reference -->
3+
<package >
4+
<metadata>
5+
<id>RedGate.ULibs.ConcurrentForEach.Sources</id>
6+
<version>$version$</version>
7+
<authors>red-gate</authors>
8+
<owners>red-gate</owners>
9+
<requireLicenseAcceptance>false</requireLicenseAcceptance>
10+
<summary><![CDATA[$summary$]]></summary>
11+
<description><![CDATA[$description$]]></description>
12+
<releaseNotes><![CDATA[$releaseNotes$]]></releaseNotes>
13+
<copyright>Copyright $copyrightYear$ Red Gate Software Ltd</copyright>
14+
<projectUrl>https://github.com/red-gate/MicroLibraries</projectUrl>
15+
</metadata>
16+
<files>
17+
<file src="*.pp" target="content\App_Packages\RedGate.ULibs.ConcurrentForEach.$version$"/>
18+
</files>
19+
</package>

0 commit comments

Comments
 (0)