异步任务似乎 运行 同步

Async Tasks Seem to Run Synchronously

Run 内,我正在尝试对 10,000 个任务进行排队。 (这只是一个实验,所以我可以更好地理解 async await。)但是第一个循环需要三秒钟才能完成。我希望这会更快,因为我只是想对任务进行排队,并在几行之后 await 它们。仍然在 Run 内,alreadyCompleted 是正确的,因为任务似乎有 运行 同步。最后,仍然在 Run 内,第二个循环需要 1 毫秒才能完成,再次显示任务已经 运行.

如何将任务排队到 运行,同时仍然允许执行通过 Run 方法,直到我使用 await?如果我的一些任务在 alreadyCompleted 被检查时已经完成,我会理解,但所有任务都已完成似乎很奇怪。此行为在 ASP.NET Core 3、.NET Core 3 控制台和 .NET Framework 控制台之间是一致的。谢谢。

using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

public static class SqlUtility
{
    public static async Task Run()
    {
        const int max = 10000;
        Stopwatch sw1 = Stopwatch.StartNew();
        Task<int>[] tasks = new Task<int>[max];
        for(int i=0;i<max;i++)
        {
            tasks[i] = GetVideoID();
        }
        sw1.Stop();//ElapsedMilliseconds:  3169
        bool alreadyCompleted = tasks.All(t => t.IsCompletedSuccessfully);//true
        Stopwatch sw2 = Stopwatch.StartNew();
        for (int i = 0; i < max; i++)
        {
            await tasks[i].ConfigureAwait(false);
        }
        sw2.Stop();//ElapsedMilliseconds:  1
    }

    public static async Task<int> GetVideoID()
    {
        const string connectionString =
            "Server=localhost;Database=[Redacted];Integrated Security=true";
        SqlParameter[] parameters = new SqlParameter[]
        {
            new SqlParameter("VideoID", SqlDbType.Int) { Value = 1000 }
        };
        const string commandText = "select * from Video where VideoID=@VideoID";
        IAsyncEnumerable<object> values = GetValuesAsync(connectionString, parameters, commandText,
            CancellationToken.None);
        object videoID = await values.FirstAsync().ConfigureAwait(false);//1000
        return (int)videoID;
    }

    public static async IAsyncEnumerable<object> GetValuesAsync(
        string connectionString, SqlParameter[] parameters, string commandText,
        [EnumeratorCancellation]CancellationToken cancellationToken)
    {
        using (SqlConnection connection = new SqlConnection(connectionString))
        {
            await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
            using (SqlCommand command = new SqlCommand(commandText, connection))
            {
                command.Parameters.AddRange(parameters);
                using (var reader = await command.ExecuteReaderAsync()
                    .ConfigureAwait(false))
                {
                    while (await reader.ReadAsync().ConfigureAwait(false))
                    {
                        yield return reader[0];
                    }
                }
            }
        }
    }
}

更新

虽然 运行 使用 .NET Core 应用程序并使用 1,000 的 max,但 alreadyCompleted 是错误的。事实上,查看每个任务的 IsCompleted,none 的任务已完成,而当前 alreadyCompleted。这是我原先设想的情况。

但是如果 max 设置为 10,000(因为它是原来的),alreadyCompleted 是假的,就像上面一样。有趣的是,检查 tasks[max - 1].IsCompleted 结果是 false (首先检查最后一个任务)。 tasks.All(t => t.IsCompletedSuccessfully) 最后检查最后一个结果,此时 IsCompletedtrue.

了解 async 方法同步启动 运行ning 很重要。当 await 被赋予 不完整 Task 时,奇迹就发生了。 (如果给它 completed Task,执行会同步继续)

因此,当您调用 GetVideoID 时,这就是它正在做的事情:

  1. GetVideoID 运行 秒直到 GetValuesAsync()
  2. GetValuesAsync 运行 秒直到 await connection.OpenAsync
  3. OpenAsync runs until it returns a Task.
  4. GetValuesAsync中的await看到不完整的Task和returns自己不完整的Task.
  5. GetVideoID 的执行将继续,直到 records.FirstAsync() 被调用。
  6. FirstAsync() 运行s 直到它 returns 一个不完整的 Task.
  7. GetVideoID中的await看到不完整的Task和returns自己不完整的Task.
  8. Run 恢复执行。

所有这些都是同步发生的。三秒钟做 10,000 次是相当合理的。

那么,为什么当您检查它们时,每个 Task 都已经完成了?简短的回答是,这些任务的延续 运行 在其他线程上。

如果这不是 Web 应用程序(或者如果它是 .NET Framework),那么就是 ConfigureAwait(false) 实现了这一点。否则,延续将不得不等到 Run() 暂停执行。但是对于 ConfigureAwait(false),延续可以在任何上下文中 运行,因此它们在 ThreadPool 线程上 运行。

如果这是ASP.NET核心,那does not have any synchronization context, it would happen anyway even without the ConfigureAwait(false). That article talks about this under the heading Beware Implicit Parallelism:

ASP.NET Core does not have a SynchronizationContext, so await defaults to the thread pool context. So, in the ASP.NET Core world, asynchronous continuations may run on any thread, and they may all run in parallel.

总结:

  • 运行 这 10,000 个任务的初始同步部分需要 3 秒。
  • 这些任务的延续全部 运行 在其他线程上并行进行。
  • 当您检查时,它们都已完成。因此,"awaiting"根本不需要等待。

更新: 如果你想避免最初的 3 秒延迟,那么你可以使用 Task.Run 在另一个线程上启动任务,这将允许你方法几乎立即继续。只需替换此:

tasks[i] = GetVideoID();

有了这个:

tasks[i] = Task.Run(() => GetVideoID());

但是,这会产生开销,因此您应该计算完成整个操作需要多长时间,看看它是否真的对您有帮助。

虽然这在 ASP.NET 中可能不是一个好主意,因为可用的线程数量有限。