为什么await async这么慢?

本文关键字:await async 为什么 | 更新日期: 2023-09-27 18:06:04

我终于得到了VS2012,并得到了一个简单的演示,并开始工作,以检查async和await的潜在性能提升,但令我沮丧的是它更慢!我可能做错了什么,但也许你能帮我一把。(我还添加了一个简单的线程解决方案,运行速度比预期的要快)

我的代码使用一个类来求和数组,基于系统上的核心数量(-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;
        }
    }
}

任何想法吗?我做async/await错误吗?我很乐意尝试任何建议。

为什么await async这么慢?

将"异步"与"并行化"分开是很重要的。await的存在是为了帮助更容易地编写异步代码。并行运行的代码可能包含(也可能不包含)异步,而异步的代码可能包含也可能不包含并行运行。

await不是为了使并行代码更快而设计的。await的目的是使编写异步代码更容易,同时尽量减少对性能的负面影响。使用await不会比正确编写的非等待异步代码更快(尽管因为使用await编写正确的代码更容易,但有时会更快,因为程序员无法在没有await的情况下正确编写异步代码,或者不愿意花时间这样做。如果非异步代码写得好,它的性能将与await代码一样好,如果不是稍微好一点的话。

c#确实有专门的并行化支持,只是没有通过await。任务并行库(TPL)和并行LINQ (PLINQ)有几种非常有效的并行代码方法,通常比简单的线程实现更有效。

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

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

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

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

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

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

  • 你正在计时第一次运行,其中包括初始化时间(加载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现在是最快的解决方案,只需不到2ms。

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

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

快速查看一下,结果是预期的:您的异步求和只使用一个线程,而您异步等待它完成,因此它比多线程求和慢。

你会使用async,以防你有其他事情要完成,而它正在做它的工作。因此,这不是测试任何速度/响应改进的正确方法。