Created
January 31, 2020 13:32
-
-
Save tmenier/4c2571a4e3ea99c2dead97e416f8a4a4 to your computer and use it in GitHub Desktop.
Task.WhenAll on steroids
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static class Throttler | |
{ | |
/// <summary> | |
/// Concurrently run a task for each data item. Like Task.WhenAll, except you can cap the number allowed to run at a time, | |
/// and enforce a minimum pause between the start of each. | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
/// <param name="items">A list of data items.</param> | |
/// <param name="minPause">Minimum wait time in milliseconds between starting tasks.</param> | |
/// <param name="maxConcurrent">Maximum tasks allowed to run at the same time.</param> | |
/// <param name="op">An async operation to perform on each data item.</param> | |
/// <returns></returns> | |
public static async Task RunAsync<T>(IEnumerable<T> items, int minPause, int maxConcurrent, Func<T, Task> op) { | |
using (var sem = new SemaphoreSlim(maxConcurrent)) { | |
async Task RunOneAsync(T item) { | |
try { await op(item); } | |
finally { sem.Release(); } | |
} | |
var tasks = new List<Task>(); | |
foreach (var item in items) { | |
if (tasks.Any()) // avoid pausing before the first one | |
await Task.WhenAll(sem.WaitAsync(), Task.Delay(minPause)); // wait until we're under the concurrency limit AND at least minPause has passed | |
tasks.Add(RunOneAsync(item)); | |
} | |
await Task.WhenAll(tasks); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The advantages of this solution over processing items in batches (using
Task.WhenAll
) is 2-fold:The ability to add a brief pause before the start of each Task.
The system is better at maintaining the maximum allowed concurrency at all times. For example, if you're processing items in batches of 10, then items 11-20 would start (all at once) only after the number of running Tasks in the first batch is down to zero. With this solution, item 11 is allowed to start as soon as the number of running Tasks is down to 9.