【问题标题】:Why is await async so slow?为什么等待异步这么慢?
【发布时间】:2013-03-17 22:25:46
【问题描述】:

我终于得到了 VS2012 并得到了一个简单的演示,并正在努力检查异步和等待的潜在性能提升,但令我沮丧的是它更慢!可能我做错了什么,但也许你可以帮助我。 (我还添加了一个简单的 Threaded 解决方案,并且运行速度符合预期)

我的代码根据系统上的内核数(-1)使用一个类来对数组求和为同样的事情而失败,但使用 async/await。

代码:(请注意,您需要添加对System.Management 的引用才能使核心检测器正常工作)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Management;
using System.Diagnostics;

namespace AsyncSum
{
    class Program
    {
        static string Results = "";

        static void Main(string[] args)
        {
            Task t = Run();
            t.Wait();

            Console.WriteLine(Results);
            Console.ReadKey();
        }

        static async Task Run()
        {
            Random random = new Random();

            int[] huge = new int[1000000];

            for (int i = 0; i < huge.Length; i++)
            {
                huge[i] = random.Next(2);
            }

            ArraySum summer = new ArraySum(huge);

            Stopwatch sw = new Stopwatch();

            sw.Restart();
            long tSum = summer.Sum();
            for (int i = 0; i < 100; i++)
            {
                tSum = summer.Sum();
            }
            long tticks = sw.ElapsedTicks / 100;

            long aSum = await summer.SumAsync();
            sw.Restart();
            for (int i = 0; i < 100; i++)
            {
                aSum = await summer.SumAsync();
            }
            long aticks = sw.ElapsedTicks / 100;

            long dSum = summer.SumThreaded();
            sw.Restart();
            for (int i = 0; i < 100; i++)
            {
                dSum = summer.SumThreaded();
            }
            long dticks = sw.ElapsedTicks / 100;


            long pSum = summer.SumParallel();
            sw.Restart();
            for (int i = 0; i < 100; i++)
            {
                pSum = summer.SumParallel();
            }
            long pticks = sw.ElapsedTicks / 100;

            Program.Results += String.Format("Regular Sum: {0} in {1} ticks\n", tSum, tticks);
            Program.Results += String.Format("Async Sum: {0} in {1} ticks\n", aSum, aticks);
            Program.Results += String.Format("Threaded Sum: {0} in {1} ticks\n", dSum, dticks);
            Program.Results += String.Format("Parallel Sum: {0} in {1} ticks\n", pSum, pticks);
        }
    }

    class ArraySum
    {
        int[] Data;
        int ChunkSize = 1000;
        int cores = 1;


        public ArraySum(int[] data)
        {
            Data = data;

            cores = 0;
            foreach (var item in new System.Management.ManagementObjectSearcher("Select * from Win32_Processor").Get())
            {
                cores += int.Parse(item["NumberOfCores"].ToString());
            }
            cores--;
            if (cores < 1) cores = 1;

            ChunkSize = Data.Length / cores + 1;
        }

        public long Sum()
        {
            long sum = 0;
            for (int i = 0; i < Data.Length; i++)
            {
                sum += Data[i];
            }
            return sum;
        }

        public async Task<long> SumAsync()
        {
            Task<long>[] psums = new Task<long>[cores];
            for (int i = 0; i < psums.Length; i++)
            {
                int start = i * ChunkSize;
                int end = start + ChunkSize;

                psums[i] = Task.Run<long>(() =>
                {
                    long asum = 0;
                    for (int a = start; a < end && a < Data.Length; a++)
                    {
                        asum += Data[a];
                    }
                    return asum;
                });
            }

            long sum = 0;
            for (int i = 0; i < psums.Length; i++)
            {
                sum += await psums[i];
            }

            return sum;
        }

        public long SumThreaded()
        {
            long sum = 0;
            Thread[] threads = new Thread[cores];
            long[] buckets = new long[cores];
            for (int i = 0; i < cores; i++)
            {
                int start = i * ChunkSize;
                int end = start + ChunkSize;
                int bucket = i;
                threads[i] = new Thread(new ThreadStart(() =>
                {
                    long asum = 0;
                    for (int a = start; a < end && a < Data.Length; a++)
                    {
                        asum += Data[a];
                    }
                    buckets[bucket] = asum;
                }));
                threads[i].Start();
            }

            for (int i = 0; i < cores; i++)
            {
                threads[i].Join();
                sum += buckets[i];
            }

            return sum;
        }

        public long SumParallel()
        {
            long sum = 0;
            long[] buckets = new long[cores];
            ParallelLoopResult lr = Parallel.For(0, cores, new Action<int>((i) =>
            {
                int start = i * ChunkSize;
                int end = start + ChunkSize;
                int bucket = i;
                long asum = 0;
                for (int a = start; a < end && a < Data.Length; a++)
                {
                    asum += Data[a];
                }
                buckets[bucket] = asum;
            }));

            for (int i = 0; i < cores; i++)
            {
                sum += buckets[i];
            }

            return sum;
        }
    }
}

有什么想法吗?我做错了异步/等待吗?我很乐意尝试任何建议。

【问题讨论】:

  • 好吧,首先,您的线程示例实际上不起作用。您没有同步对聚合总和变量的访问,那里存在竞争条件。
  • 我在这里可能是错的,但我不认为它实际上是在使用多核。如果我了解我所读到的有关 C# 异步内容的内容,那么它所做的只是围绕每个任务创建状态机,而不是实际上进行任何并行处理。它只是围绕完成包装事件处理程序,所以一个核心仍在完成所有工作,只是有一堆额外的开销。您需要使用并行库才能看到差异。但我可能错了 - 仍然试图围绕所有异步和并行的东西。
  • @JoeEnos:对 Task.Run 的调用将任务调度到线程池。
  • @ohmusama:你应该使用Stopwatch,而不是DateTime
  • 仔细看看你的程序:它不是比较“async”和“Join”;它正在测量 调度到线程池 vs 只是创建一大堆线程。 “等待”与它无关。

标签: c# multithreading performance .net-4.5 async-await


【解决方案1】:

您的基准有几个缺陷:

  • 您正在计时第一次运行,其中包括初始化时间(加载 class Task、JIT 编译等)
  • 您正在使用DateTime.Now,这对于毫秒范围内的计时来说太不准确了。您需要使用StopWatch

解决了这两个问题;我得到以下基准测试结果:

Regular Sum:  499946 in 00:00:00.0047378
Async Sum:    499946 in 00:00:00.0016994
Threaded Sum: 499946 in 00:00:00.0026898

Async 现在是最快的解决方案,耗时不到 2 毫秒。

这是下一个问题:像 2ms 这样快的时间是非常不可靠的;如果其他进程在后台使用 CPU,您的线程可能会暂停更长的时间。您应该对数千次基准运行的结果进行平均。

另外,您的核心检测数量是怎么回事?我的四核使用的是 333334 的块大小,它只允许运行 3 个线程。

【讨论】:

  • 基准测试的问题不止于此。您还没有做足够的工作使结果具有统计意义。 run 方法应该被调用几千次,并且对初学者的结果取平均值,GC 收集应该在计时之间运行,预热运行代码以确保应该完成所有事情是 JITted,我相信还有很多其他的事情。说了这么多,问题本身是有缺陷的(根据我的回答),而不仅仅是基准测试。
  • 我发布的结果在多次运行中相当稳定;但是你是对的,你应该进行数千次运行,这样你就不会冒上下文切换完全搞砸你的结果的风险;我已经编辑了我的答案。
  • @Daniel,您实际上在我的评论中给出了正确的答案,如果您可以在此处将其添加到您的答案中,我会给您一个赞成票。问题是,该 lambda 表达式必须在运行时编译,因此第一次运行非常慢,但第二次运行比线性总和快。感谢您找到它!
  • @ohmusama:这就是我所说的第一点(初始化时间)。而且它与 lambda 表达式的使用无关(初始化 lambda 并不比初始化普通方法更昂贵),只是需要加载更多代码才能使整个 async/await 机器工作。跨度>
【解决方案2】:

async 不适用于重型并行计算。您可以使用Task.RunTask.WhenAll 进行基本的并行工作,但是任何严重的并行工作都应该使用任务并行库(例如Parallel)来完成。客户端的异步代码是关于响应性,而不是并行处理

一种常见的方法是使用Parallel 进行并行 工作,然后将其包装在Task.Run 中并在其上使用await 以保持UI 响应。

【讨论】:

  • @ohmusama 不是。 Parallel.For 并不是特别擅长并行聚合工作,因此根据我的示例,使用 PLINQ 会做得更好。您的循环与其他示例的功能并没有太大的不同,而不是一个非聚合函数,其中集合中的每个项目都是独立处理的,在这种情况下,Paralell.For 提供数据的能力比您可以动态确定适当的工人数量,这将帮助您看到明显的改进。
  • Parallel.For 可以聚合,但语法很尴尬。对于一个简单的求和示例,PLINQ 更好。
  • @StephenCleary 在我看来,这真的不像是计算密集型工作
【解决方案3】:

将“异步”与“并行”区分开来很重要。 await 可以帮助您更轻松地编写异步代码。并行运行的代码可能(也可能不)涉及异步,异步代码可能会或可能不会并行运行。

await 没有任何东西旨在使并行代码更快。 await 的目的是让编写异步代码更容易,同时尽量减少对性能的负面影响。使用await 永远不会比正确编写非等待异步代码更快(尽管因为使用await 编写正确代码更容易,但有时会更快,因为程序员无法正确编写异步代码等待,或者不愿意花时间这样做。如果非异步代码编写得好,它的性能与await 代码差不多,甚至更好。

C# 确实支持并行化,但不是特别支持await。任务并行库 (TPL) 和并行 LINQ (PLINQ) 有几种非常有效的并行代码方法,这些方法通常比简单的线程实现更有效。

在您的情况下,使用 PLINQ 的有效实现可能是这样的:

public static int Sum(int[] array)
{
    return array.AsParallel().Sum();
}

请注意,这将有效地将输入序列划分为并行运行的块;它将负责确定块的适当大小和并发工作人员的数量,并将适当地聚合这些工作人员的结果,以确保正确同步以确保正确的结果(与您的线程示例不同)和高效(意味着它不会完全序列化所有聚合)。

【讨论】:

  • @ohmusama,不重新发明轮子有很多好处+1,除非您对数据有所了解,否则这与您想出的任何数据一样快。它的打字速度很快,而且很容易正确。如果数据是根据已知函数分发的,您可能可以将操作短路,但这不适用于此处。
  • 我喜欢我可以做一个并行求和,但是,我想知道这样做的完整方法。所以我添加了一个并行测试,这是一个很好的例子吗?
  • 我想,鉴于我们对数据生成的了解,最好将答案近似为1000000
  • 此解决方案比单线程解决方案!不知道为什么;也许AsParallel() 强制对阵列的访问通过IEnumerator,从而增加大量开销?
  • @Daniel 好吧,我首先要问的是你的基准测试方法,因为基准测试很难,人们通常不会做对。接下来,设置所有这些都涉及开销,如果花在生产性工作上的时间足够小,那么开销可能会超过并行化带来的收益;添加更多工作或成本更高的工作将有助于减轻基准测试中的开销。
【解决方案4】:

快速浏览一下,结果是预期的:您的异步 sum 仅使用一个线程,而您异步等待它完成,因此它比多线程 sum 慢。

您可以使用 async 以防您在其工作时还有其他事情要完成。因此,这不是任何速度/响应改进的正确测试。

【讨论】:

  • +1。并且在 async/await 案例中单线程解释了速度下降 - 运行的代码比基本的 Sum 调用要多得多,以支持所有 async 基础设施。
猜你喜欢
  • 2022-01-17
  • 1970-01-01
  • 2020-09-12
  • 2016-02-16
  • 1970-01-01
  • 2017-08-02
  • 1970-01-01
  • 1970-01-01
  • 2019-10-16
相关资源
最近更新 更多