|
1 | | -namespace EnumerableAsyncProcessor.Extensions; |
| 1 | +using System.Collections.Concurrent; |
| 2 | + |
| 3 | +namespace EnumerableAsyncProcessor.Extensions; |
2 | 4 |
|
3 | 5 | public static class ParallelExtensions |
4 | 6 | { |
5 | | - public static async Task InParallelAsync<TSource, TResult>( |
| 7 | + public static Task InParallelAsync<TSource, TResult>( |
6 | 8 | this IEnumerable<TSource> source, |
7 | 9 | int levelOfParallelism, |
8 | 10 | Func<TSource, Task<TResult>> taskSelector) |
9 | 11 | { |
10 | | - await InParallelAsync(source, levelOfParallelism, taskSelector, CancellationToken.None).ConfigureAwait(false); |
| 12 | + return InParallelAsync(source, levelOfParallelism, taskSelector, CancellationToken.None); |
11 | 13 | } |
12 | 14 |
|
13 | | - public static async Task InParallelAsync<TSource, TResult>( |
| 15 | + public static Task InParallelAsync<TSource, TResult>( |
14 | 16 | this IEnumerable<TSource> source, |
15 | 17 | int levelOfParallelism, |
16 | 18 | Func<TSource, Task<TResult>> taskSelector, |
17 | 19 | CancellationToken cancellationToken) |
18 | 20 | { |
19 | | - if (levelOfParallelism <= 0) |
20 | | - { |
21 | | - levelOfParallelism = Environment.ProcessorCount; |
22 | | - } |
23 | | - |
24 | | - using var parallelLock = new SemaphoreSlim(initialCount:levelOfParallelism, maxCount:levelOfParallelism); |
25 | | - |
26 | | - await Task.WhenAll(source.Select(item => ProcessAsync(item, taskSelector, parallelLock, cancellationToken))).ConfigureAwait(false); |
| 21 | + return StartWorkers(source, levelOfParallelism, taskSelector, cancellationToken); |
27 | 22 | } |
28 | | - |
29 | | - public static async Task InParallelAsync<TSource>( |
| 23 | + |
| 24 | + public static Task InParallelAsync<TSource>( |
30 | 25 | this IEnumerable<TSource> source, |
31 | 26 | int levelOfParallelism, |
32 | 27 | Func<TSource, Task> taskSelector) |
33 | 28 | { |
34 | | - await InParallelAsync(source, levelOfParallelism, taskSelector, CancellationToken.None).ConfigureAwait(false); |
| 29 | + return InParallelAsync(source, levelOfParallelism, taskSelector, CancellationToken.None); |
35 | 30 | } |
36 | 31 |
|
37 | | - public static async Task InParallelAsync<TSource>( |
| 32 | + public static Task InParallelAsync<TSource>( |
38 | 33 | this IEnumerable<TSource> source, |
39 | 34 | int levelOfParallelism, |
40 | 35 | Func<TSource, Task> taskSelector, |
41 | 36 | CancellationToken cancellationToken) |
42 | 37 | { |
43 | | - if (levelOfParallelism <= 0) |
44 | | - { |
45 | | - levelOfParallelism = Environment.ProcessorCount; |
46 | | - } |
47 | | - |
48 | | - using var parallelLock = new SemaphoreSlim(initialCount:levelOfParallelism, maxCount:levelOfParallelism); |
49 | | - |
50 | | - await Task.WhenAll(source.Select(item => ProcessAsync(item, taskSelector, parallelLock, cancellationToken))).ConfigureAwait(false); |
| 38 | + return StartWorkers(source, levelOfParallelism, taskSelector, cancellationToken); |
51 | 39 | } |
52 | 40 |
|
53 | 41 | // Overloads for CPU-bound processing |
54 | | - public static async Task InParallelAsync<TSource, TResult>( |
| 42 | + public static Task InParallelAsync<TSource, TResult>( |
55 | 43 | this IEnumerable<TSource> source, |
56 | 44 | int levelOfParallelism, |
57 | 45 | Func<TSource, TResult> taskSelector, |
58 | 46 | CancellationToken cancellationToken = default) |
59 | 47 | { |
60 | | - await InParallelAsync(source, levelOfParallelism, item => Task.FromResult(taskSelector(item)), cancellationToken).ConfigureAwait(false); |
| 48 | + return StartWorkers( |
| 49 | + source, |
| 50 | + levelOfParallelism, |
| 51 | + item => |
| 52 | + { |
| 53 | + _ = taskSelector(item); |
| 54 | + return Task.CompletedTask; |
| 55 | + }, |
| 56 | + cancellationToken); |
61 | 57 | } |
62 | 58 |
|
63 | | - public static async Task InParallelAsync<TSource>( |
| 59 | + public static Task InParallelAsync<TSource>( |
64 | 60 | this IEnumerable<TSource> source, |
65 | 61 | int levelOfParallelism, |
66 | 62 | Action<TSource> taskSelector, |
67 | 63 | CancellationToken cancellationToken = default) |
68 | 64 | { |
69 | | - await InParallelAsync(source, levelOfParallelism, item => { taskSelector(item); return Task.CompletedTask; }, cancellationToken).ConfigureAwait(false); |
| 65 | + return StartWorkers( |
| 66 | + source, |
| 67 | + levelOfParallelism, |
| 68 | + item => |
| 69 | + { |
| 70 | + taskSelector(item); |
| 71 | + return Task.CompletedTask; |
| 72 | + }, |
| 73 | + cancellationToken); |
70 | 74 | } |
71 | 75 |
|
72 | | - private static async Task<TResult> ProcessAsync<TSource, TResult>( |
73 | | - TSource item, |
74 | | - Func<TSource, Task<TResult>> taskSelector, |
75 | | - SemaphoreSlim parallelLock, |
| 76 | + private static Task StartWorkers<TSource>( |
| 77 | + IEnumerable<TSource> source, |
| 78 | + int levelOfParallelism, |
| 79 | + Func<TSource, Task> taskSelector, |
76 | 80 | CancellationToken cancellationToken) |
77 | 81 | { |
78 | | - var semaphoreAcquired = false; |
79 | | - |
| 82 | + TSource[] items; |
| 83 | + |
80 | 84 | try |
81 | 85 | { |
82 | | - await parallelLock.WaitAsync(cancellationToken).ConfigureAwait(false); |
83 | | - semaphoreAcquired = true; |
84 | | - |
85 | | - cancellationToken.ThrowIfCancellationRequested(); |
| 86 | + items = source.ToArray(); |
| 87 | + } |
| 88 | + catch (Exception exception) |
| 89 | + { |
| 90 | + return Task.FromException(exception); |
| 91 | + } |
86 | 92 |
|
87 | | - return await taskSelector(item).ConfigureAwait(false); |
| 93 | + if (items.Length == 0) |
| 94 | + { |
| 95 | + return Task.CompletedTask; |
88 | 96 | } |
89 | | - finally |
| 97 | + |
| 98 | + if (levelOfParallelism <= 0) |
90 | 99 | { |
91 | | - if (semaphoreAcquired) |
| 100 | + levelOfParallelism = Environment.ProcessorCount; |
| 101 | + } |
| 102 | + |
| 103 | + var workerCount = Math.Min(levelOfParallelism, items.Length); |
| 104 | + var completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); |
| 105 | + var exceptions = new ConcurrentQueue<Exception>(); |
| 106 | + var nextIndex = -1; |
| 107 | + var remainingWorkers = workerCount; |
| 108 | + var wasCanceled = 0; |
| 109 | + |
| 110 | + for (var i = 0; i < workerCount; i++) |
| 111 | + { |
| 112 | + _ = Task.Run(async () => |
| 113 | + { |
| 114 | + try |
| 115 | + { |
| 116 | + while (true) |
| 117 | + { |
| 118 | + if (cancellationToken.IsCancellationRequested) |
| 119 | + { |
| 120 | + Interlocked.Exchange(ref wasCanceled, 1); |
| 121 | + return; |
| 122 | + } |
| 123 | + |
| 124 | + var index = Interlocked.Increment(ref nextIndex); |
| 125 | + |
| 126 | + if (index >= items.Length) |
| 127 | + { |
| 128 | + return; |
| 129 | + } |
| 130 | + |
| 131 | + Task? task = null; |
| 132 | + |
| 133 | + try |
| 134 | + { |
| 135 | + cancellationToken.ThrowIfCancellationRequested(); |
| 136 | + task = taskSelector(items[index]); |
| 137 | + await task.ConfigureAwait(false); |
| 138 | + } |
| 139 | + catch (OperationCanceledException) |
| 140 | + { |
| 141 | + Interlocked.Exchange(ref wasCanceled, 1); |
| 142 | + } |
| 143 | + catch (Exception exception) |
| 144 | + { |
| 145 | + EnqueueExceptions(exceptions, task, exception); |
| 146 | + } |
| 147 | + } |
| 148 | + } |
| 149 | + catch (Exception exception) |
| 150 | + { |
| 151 | + exceptions.Enqueue(exception); |
| 152 | + } |
| 153 | + finally |
| 154 | + { |
| 155 | + if (Interlocked.Decrement(ref remainingWorkers) == 0) |
| 156 | + { |
| 157 | + Complete(completionSource, exceptions, wasCanceled, cancellationToken); |
| 158 | + } |
| 159 | + } |
| 160 | + }); |
| 161 | + } |
| 162 | + |
| 163 | + return completionSource.Task; |
| 164 | + } |
| 165 | + |
| 166 | + private static void EnqueueExceptions( |
| 167 | + ConcurrentQueue<Exception> exceptions, |
| 168 | + Task? task, |
| 169 | + Exception exception) |
| 170 | + { |
| 171 | + if (task is { IsFaulted: true }) |
| 172 | + { |
| 173 | + foreach (var innerException in task.Exception!.InnerExceptions) |
92 | 174 | { |
93 | | - parallelLock.Release(); |
| 175 | + exceptions.Enqueue(innerException); |
94 | 176 | } |
| 177 | + |
| 178 | + return; |
95 | 179 | } |
| 180 | + |
| 181 | + exceptions.Enqueue(exception); |
96 | 182 | } |
97 | | - |
98 | | - private static async Task ProcessAsync<TSource>( |
99 | | - TSource item, |
100 | | - Func<TSource, Task> taskSelector, |
101 | | - SemaphoreSlim parallelLock, |
| 183 | + |
| 184 | + private static void Complete( |
| 185 | + TaskCompletionSource completionSource, |
| 186 | + ConcurrentQueue<Exception> exceptions, |
| 187 | + int wasCanceled, |
102 | 188 | CancellationToken cancellationToken) |
103 | 189 | { |
104 | | - var semaphoreAcquired = false; |
105 | | - |
106 | | - try |
| 190 | + if (!exceptions.IsEmpty) |
| 191 | + { |
| 192 | + completionSource.TrySetException(exceptions); |
| 193 | + } |
| 194 | + else if (wasCanceled != 0) |
107 | 195 | { |
108 | | - await parallelLock.WaitAsync(cancellationToken).ConfigureAwait(false); |
109 | | - semaphoreAcquired = true; |
110 | | - |
111 | | - cancellationToken.ThrowIfCancellationRequested(); |
| 196 | + var canceledToken = cancellationToken.IsCancellationRequested |
| 197 | + ? cancellationToken |
| 198 | + : new CancellationToken(canceled: true); |
112 | 199 |
|
113 | | - await taskSelector(item).ConfigureAwait(false); |
| 200 | + completionSource.TrySetCanceled(canceledToken); |
114 | 201 | } |
115 | | - finally |
| 202 | + else |
116 | 203 | { |
117 | | - if (semaphoreAcquired) |
118 | | - { |
119 | | - parallelLock.Release(); |
120 | | - } |
| 204 | + completionSource.TrySetResult(); |
121 | 205 | } |
122 | 206 | } |
123 | 207 | } |
0 commit comments