You can have millions of long-running tasks, but you can't have millions of long-running threads (unless you own a machine with terabytes of RAM, since each thread allocates 1 MB). The way to have so many tasks is to make them async. Instead of having them sleeping with Thread.Sleep
, you can have them awaiting asynchronously a Task.Delay
. Here is an example:
var cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;
Task[] tasks = Enumerable.Range(1, 1_000_000).Select(index => Task.Run(async () =>
{
await Task.Delay(index, ct); // Initial delay to spread things out
while (true)
{
var webResult = await WebCallAsync(index, ct); // asynchronous web call
await DbUpdateAsync(webResult, ct); // update result in DB
await Task.Delay(1000 * 60 * 10, ct); // do nothing for 10 minutes
}
})).ToArray();
Task.WaitAll(tasks);
The purpose of the CancellationTokenSource
is for cancelling all tasks at any time by calling cts.Cancel()
. Combining Task.Delay
with cancellation creates an unexpected overhead though, because the cancellation is propagated through OperationCanceledException
exceptions, and one million exceptions cause considerable stress to the .NET infrastructure. In my PC the overhead is about 50 seconds of 100% CPU consumption. If you do like the idea of using CancellationToken
s, a workaround is to use an alternative Task.Delay
that doesn't throw exceptions. Here is an implementation of this idea:
/// <summary>Returns a <see cref="Task"/> that will complete with a result of true
/// if the specified number of milliseconds elapsed successfully, or false
/// if the cancellation token is canceled.</summary>
private static async Task<bool> NonThrowingDelay(int millisecondsDelay,
CancellationToken cancellationToken = default)
{
if (cancellationToken.IsCancellationRequested) return false;
if (millisecondsDelay == 0) return true;
var tcs = new TaskCompletionSource<bool>();
using (cancellationToken.Register(() => tcs.TrySetResult(false)))
using (new Timer(_ => tcs.TrySetResult(true), null, millisecondsDelay, Timeout.Infinite))
return await tcs.Task.ConfigureAwait(false);
}
And here is how you could use the NonThrowingDelay
method for creating 1,000,000 tasks that can be canceled (almost) instantly:
var cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;
Task[] tasks = Enumerable.Range(1, 1_000_000).Select(index => Task.Run(async () =>
{
if (!await NonThrowingDelay(index, ct)) return; // Initial delay
while (true)
{
var webResult = await WebCallAsync(index, ct); // asynchronous web call
await DbUpdateAsync(webResult, ct); // update result in DB
if (!await NonThrowingDelay(1000 * 60 * 10, ct)) break; // 10 minutes
}
})).ToArray();
Task.WaitAll(tasks);