为什么等待异步这么慢?

ohm*_*ama 17 c# performance multithreading async-await .net-4.5

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

我的代码使用一个类来根据系统中的内核数量对数组求和(-1)我有4个内核,所以我看到了大约2倍的加速(2.5个线程)用于线程,但是减少了2倍的速度同样的事情,但使用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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Ser*_*rvy 25

将"异步"与"并行化"分开是很重要的. await有助于简化编写异步代码.并行运行的代码可能(或可能不)涉及异步,并且异步的代码可能会或可能不会并行运行.

没有什么await可以使并行代码更快.目的await是使编写异步代码更容易,同时最大限度地减少负面性能影响.使用await永远不会比正确编写的非等待异步代码更快(尽管因为编写正确的代码await更容易,它有时会更快,因为程序员无法正常编写异步代码而不等待,或者不是愿意花时间这样做.如果非同步代码编写得很好,那么它的表现也会比await代码更好,如果不是更好的话.

C#确实支持专门用于并行化,但它并不是特别的await.任务并行库(TPL)以及并行LINQ(PLINQ)具有几种非常有效的并行化代码的方法,通常比天真的线程实现更有效.

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

public static int Sum(int[] array)
{
    return array.AsParallel().Sum();
}
Run Code Online (Sandbox Code Playgroud)

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


Ste*_*ary 11

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

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


Dan*_*iel 8

您的基准测试有几个缺陷:

  • 你是第一次运行的计时,包括初始化时间(加载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
Run Code Online (Sandbox Code Playgroud)

Async现在成为最快的解决方案,耗时不到2毫秒.

这是下一个问题:时间快到2毫秒非常不可靠; 如果某个其他进程在后台使用CPU,则您的线程可以暂停更长时间.您应该将结果平均在数千个基准测试运行中.

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

  • 基准测试的问题不仅限于此.你也没有做足够的事情来使结果具有统计意义.run方法应该调用几千次,结果平均为starters,GC集合应该在时间之间运行,warmup运行代码以确保一切都是JITted应该完成,我相信很多其他事情.说了这么多,这个问题本身就存在缺陷(根据我的回答),而不仅仅是基准测试. (6认同)

Flo*_*min 5

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

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