You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+46-14Lines changed: 46 additions & 14 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -20,6 +20,39 @@ See the [benchmark guide](benchmarks/README.md) to run the BenchmarkDotNet suite
20
20
21
21
Version 4 requires .NET 8 or later. The package targets and tests `net8.0`, `net9.0`, and `net10.0`.
22
22
23
+
## Migrating from v3 to v4
24
+
25
+
Version 4 is a major release. Review these source and behaviour changes before upgrading:
26
+
27
+
-**.NET 8 is the minimum runtime.** The `net6.0` and `netstandard2.0` targets, the Polyfill dependency, and the `TaskCompletionSource` compatibility shim were removed. The package targets and tests `net8.0`, `net9.0`, and `net10.0`.
28
+
-**Parallel processing has one API.** Use `ProcessInParallel(maxConcurrency: 100)` for bounded concurrency and `ProcessInParallel()` for unbounded concurrency. The old non-timed `RateLimitedParallelAsyncProcessor*` types and duplicate overload family were removed. Rename named `levelOfParallelism` arguments to `maxConcurrency`; replace positional `ProcessInParallel(true)` calls with `ProcessInParallel(scheduleOnThreadPool: true)`.
29
+
-**Timed processing is a real start-rate limit.** It now uses a shared token bucket instead of holding each worker slot for at least one window. `ProcessInParallel(permitsPerWindow, window, maxConcurrency)` controls start rate and in-flight concurrency independently. The existing two-argument overload remains and uses its first value for both limits.
30
+
-**`IEnumerable<T>` input is materialized once when the processor is built.** One-shot enumerables are now supported and side effects run once. Iterator exceptions surface from the terminal builder call, such as `ProcessInParallel(...)`, instead of later from an awaiter.
31
+
-**Synchronous disposal no longer waits.**`Dispose()` cancels pending work and releases resources without blocking. Use `await DisposeAsync()` or `await using` when shutdown must wait for in-flight work; the async wait is bounded to 30 seconds.
32
+
-**Arbitrary upper limits were removed.** Task counts and batch sizes may exceed 10,000, and time windows may exceed 24 hours. Validity checks remain: counts, batch sizes, concurrency, and permit counts must be positive; time windows cannot be negative.
33
+
-**Validation is consistent and eager.** Invalid `maxConcurrency`, parallelism, batch, and timed-rate arguments now throw while the processor is built for both action and result variants.
34
+
-**Incidental `TaskWrapper` API was removed.**`IEquatable<T>`, equality operators, `Deconstruct`, and custom `GetHashCode` members were implementation details and are no longer public. Recompile consumers that referenced those members.
35
+
-**Synchronous `InParallelAsync` delegates now run concurrently.** The `Func<TSource, TResult>` and `Action<TSource>` overloads use thread-pool workers instead of executing delegates sequentially on the caller. Do not depend on their previous thread affinity or execution order.
36
+
37
+
Version 4 also adds cancellation-aware selectors. Use `(item, cancellationToken) => ...` when in-flight work must observe external cancellation, `CancelAll()`, or disposal:
38
+
39
+
```csharp
40
+
awaitusingvarprocessor=ids
41
+
.ForEachAsync(
42
+
async (id, cancellationToken) =>
43
+
awaitDoSomethingAsync(id, cancellationToken),
44
+
cancellationToken)
45
+
.ProcessInParallel(maxConcurrency: 100);
46
+
47
+
awaitprocessor.WaitAsync();
48
+
```
49
+
50
+
## Execution model
51
+
52
+
Bounded parallel processors use a fixed set of workers, so coordination work scales with `maxConcurrency` instead of item count. Bounded `IAsyncEnumerable<T>` processing uses a bounded channel to apply source backpressure while preserving result order. Unbounded processing starts work as input is consumed and can place substantial pressure on memory, CPU, network connections, or downstream services.
53
+
54
+
Timed processors acquire a shared token-bucket permit before starting each operation. The permit rate and maximum in-flight concurrency are separate controls; long-running operations therefore do not reduce permit replenishment. A zero-length window disables start-rate throttling but retains the concurrency limit.
55
+
23
56
## Why I built this
24
57
25
58
Because I've come across situations where you need to fine tune the rate at which you do things.
@@ -40,7 +73,7 @@ Maybe you just don't want to write all the boilerplate code that comes with mana
Processes asynchronous tasks in parallel. Pass `maxConcurrency` to bound the number of operations running at once, or omit it for unbounded concurrency.
76
+
Processes asynchronous tasks in parallel. Pass `maxConcurrency` to use a fixed worker pool and bound the number of operations running at once, or omit it for unbounded concurrency.
44
77
45
78
**Usage**
46
79
@@ -80,11 +113,9 @@ Choose a concurrency limit that protects downstream resources. Unbounded process
Processes your Asynchronous Tasks in Parallel, but honouring the limit that you set over the timespan that you set. As one finishes, another will start, unless you've hit the maximum allowed for the current timespan duration.
116
+
Uses a shared token bucket to limit how many operations may start in each window. This is useful when a downstream API has a requests-per-second limit.
84
117
85
-
E.g. If you set a limit of 100, and a timespan of 1 second, only 100 operation should ever run at any one time over the course of a second. If the operation finishes sooner than a second (or your provided timespan), it'll wait and then start the next operation once that timespan has elapsed.
86
-
87
-
This is useful in scenarios where, for example, you have an API but it has a request per second limit
118
+
The two-argument overload uses the same value for permits per window and maximum in-flight concurrency. Use the three-argument overload when those limits should differ.
88
119
89
120
**Usage**
90
121
@@ -94,19 +125,20 @@ var ids = Enumerable.Range(0, 5000).ToList();
94
125
// SelectAsync for if you want to return something - using proper disposal
- If your operations take longer than your provided TimeSpan, you probably won't get your desired throughput. This processor ensures you don't go over your rate limit, but will not increase parallel execution if you're below it.
141
+
The first example allows up to 100 starts per second and 200 in-flight operations. The second preserves the source-compatible two-argument shape and applies 100 to both limits.
110
142
111
143
### One At A Time
112
144
@@ -182,7 +214,7 @@ await ids
182
214
As above, you can see that you can just `await` on the processor to get the results.
183
215
Below shows examples of using the processor object and the various methods available.
184
216
185
-
This is for when you need to Enumerate through some objects and use them in your operations. E.g. Sending notifications to certain ids
217
+
Use an item processor when each operation needs an input value, such as sending notifications to a set of IDs.
186
218
187
219
```csharp
188
220
varhttpClient=newHttpClient();
@@ -219,7 +251,7 @@ This is for when you need to Enumerate through some objects and use them in your
219
251
}
220
252
```
221
253
222
-
This is for when you need to don't need any objects - But just want to do something a certain amount of times. E.g. Pinging a site to warm up multiple instances
254
+
Use an execution-count processor when an operation should run a fixed number of times without input values, such as warming multiple site instances.
1.**Cancellation**: Internal `CancellationTokenSource` is cancelled and any unstarted tasks complete as cancelled
348
-
2.**Task Waiting**: `await DisposeAsync()` waits up to 30 seconds for in-flight tasks to finish; synchronous `Dispose()` cancels and releases without blocking
379
+
1.**Cancellation**: The internal `CancellationTokenSource` is cancelled and unstarted tasks complete as cancelled. Token-aware selectors can cancel in-flight work.
380
+
2.**Task Waiting**: `await DisposeAsync()` waits up to 30 seconds for in-flight tasks to finish; synchronous `Dispose()` cancels and releases without blocking.
349
381
3.**Resource Cleanup**: Disposes internal resources like `CancellationTokenSource`
350
382
4.**Thread Safety**: All disposal operations are thread-safe
0 commit comments