0

在 内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 应用程序并使用max1,000 时,alreadyCompleted是错误的。实际上,查看IsCompleted每个任务,alreadyCompleted当前没有完成的任务。这是我在原始场景中所期望的。

但如果max设置为 10,000(如其最初的那样),alreadyCompleted则为假,如上所示。有趣的是,检查tasks[max - 1].IsCompleted结果false(首先检查最后一个任务)。 tasks.All(t => t.IsCompletedSuccessfully)最后检查最后一个结果,此时IsCompletedtrue.

4

1 回答 1

4

了解async方法开始同步运行很重要。当await给出一个不完整 Task的. (如果给定了completed Task,则同步继续执行)

所以当你打电话时GetVideoID,这就是它正在做的事情:

  1. GetVideoID运行到GetValuesAsync()
  2. GetValuesAsync运行到await connection.OpenAsync
  3. OpenAsync运行直到它返回一个Task.
  4. awaitinGetValuesAsync看到不完整Task并返回它自己的不完整Task
  5. GetVideoID恢复执行直到records.FirstAsync()被调用。
  6. FirstAsync()运行直到它返回一个不完整的Task.
  7. awaitinGetVideoID看到不完整Task并返回它自己的不完整Task
  8. 执行Run简历。

所有这些都是同步发生的。3 秒内完成 10,000 次是相当合理的。

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

如果这不是一个 Web 应用程序(或者如果它是 .NET Framework),那么它ConfigureAwait(false)就是让这种情况发生的原因。没有它,延续将不得不等到Run()暂停执行。但是使用ConfigureAwait(false),延续可以在任何上下文中运行,因此它们在 ThreadPool 线程上运行。

如果这是没有任何同步上下文的 ASP.NET Core,即使没有ConfigureAwait(false). 那篇文章在标题Beware Implicit Parallelism下讨论了这个问题:

ASP.NET Core 没有SynchronizationContext,因此await默认为线程池上下文。因此,在 ASP.NET Core 世界中,异步延续可以在任何线程上运行,并且它们都可以并行运行。

总之:

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

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

tasks[i] = GetVideoID();

有了这个:

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

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

尽管由于可用线程数量有限,这在 ASP.NET 中可能不是一个好主意。

于 2019-11-13T16:50:05.807 回答