异步任务似乎可以同步运行

Run中,我正在尝试将10,000个任务排队。 (这只是一个实验,所以我可以更好地理解async await。)但是第一个循环需要三秒钟才能完成。我希望这样做会更快,因为我只是尝试将任务排队,然后在几行之后awaitRun仍在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应用程序并使用max为1,000时,alreadyCompleted为假。实际上,查看每个任务的IsCompleted,当前alreadyCompleted所在的位置都没有完成任务。这就是我原本会发生的事情。

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

lflxyz 回答:异步任务似乎可以同步运行

了解async方法开始同步运行很重要。当await被赋予不完整 Task时,魔术就发生了。 (如果给出的是完成 Task,则执行将继续同步)

因此,当您致电GetVideoID时,它就是这样做的:

  1. GetVideoID一直运行到GetValuesAsync()
  2. GetValuesAsync一直运行到await connection.OpenAsync
  3. OpenAsync一直运行到returns a Task
  4. await中的GetValuesAsync看到不完整的Task,并返回自己的不完整的Task
  5. 继续执行GetVideoID,直到调用records.FirstAsync()
  6. FirstAsync()运行直到返回不完整的Task
  7. await中的GetVideoID看到不完整的Task,并返回自己的不完整的Task
  8. 恢复执行Run

所有这些都是同步发生的。花三秒钟完成10,000次是很合理的。

为什么检查每个Task时已经完成?简短的答案是这些任务的延续正在其他线程上运行。

如果这不是一个Web应用程序(或者它是.NET Framework),那么ConfigureAwait(false)就会使这种情况发生。否则,继续操作必须等到Run()的执行暂停。但是使用ConfigureAwait(false),这些延续可以在任何上下文中运行,因此它们可以在ThreadPool线程上运行。

如果这是{。{3}}的ASP.NET Core,即使没有ConfigureAwait(false),也还是会发生。该文章在标题does not have any synchronization context下对此进行了讨论:

  

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

总结:

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

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

tasks[i] = GetVideoID();

与此:

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

但是,这样做有开销,所以您应该花时间完成整个操作需要多长时间,看看它是否对您有帮助。

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

本文链接:https://www.f2er.com/3108728.html

大家都在问