哪个选项在大规模情况下更好 - 每个用户的循环任务或循环并为所有用户完成工作的单个任务?

Which option is better on large scale - a looping task per user or a single task that loops and does the job for all users?

我为每个用户创建了一个循环,该循环执行使用 Task.Factory.StartNew() 创建的轻量级任务。循环中的任务执行轻量级工作并休眠几秒钟。

如果成千上万的用户同时工作,这种代码是否有效? 管理这么多线程是否会给服务器带来很大的负载? 让一个线程为所有用户完成这项工作是否更好?

这是当前代码中发生的情况,

Task.Factory.StartNew(() =>
{
  // begin loop
  // synchronous web call
  // process and update result in DB
  // exit condition
  // sleep for few minutes
  // end loop
}

您可以拥有数百万个长运行 任务,但不能拥有数百万个长运行 线程(除非您拥有一台拥有数 TB RAM 的机器,因为每个线程分配). 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. 这是一个例子:

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);

CancellationTokenSource的目的是通过调用cts.Cancel()随时取消所有任务。但是,将 Task.Delay 与取消结合使用会产生意想不到的开销,因为取消是通过 OperationCanceledException 异常传播的,而一百万个异常会对 .NET 基础结构造成相当大的压力。在我的 PC 中,开销约为 50 秒的 100% CPU 消耗。如果您确实喜欢使用 CancellationToken 的想法,解决方法是使用不会引发异常的替代 Task.Delay。这是这个想法的实现:

/// <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);
}

下面是如何使用 NonThrowingDelay 方法创建 1,000,000 个可以(几乎)立即取消的任务:

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);