如何使用AsParallel与async和await关键字?

25
我在查看异步代码的示例时注意到它的实现存在一些问题。在查看代码时,我想知道使用并行方式循环遍历列表是否比普通方式更有效率。
据我所知,性能方面几乎没有什么区别,两种方式都会占用所有处理器,并且花费完成时间也大致相同。
下面是第一种实现方式:
var tasks= Client.GetClients().Select(async p => await p.Initialize());

这是第二个

var tasks = Client.GetClients().AsParallel().Select(async p => await p.Initialize());

我猜想两者之间没有区别,我的猜想正确吗?

完整的程序可以在下面找到。

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

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            RunCode1();
            Console.WriteLine("Here");
            Console.ReadLine();

            RunCode2();
            Console.WriteLine("Here");

            Console.ReadLine();

        }

        private async static void RunCode1()
        {
            Stopwatch myStopWatch = new Stopwatch();
            myStopWatch.Start();

            var tasks= Client.GetClients().Select(async p => await p.Initialize());

            Task.WaitAll(tasks.ToArray());
            Console.WriteLine("Time ellapsed(ms): " + myStopWatch.ElapsedMilliseconds);
            myStopWatch.Stop();
        }
        private async static void RunCode2()
        {
            Stopwatch myStopWatch = new Stopwatch();
            myStopWatch.Start();
            var tasks = Client.GetClients().AsParallel().Select(async p => await p.Initialize());
            Task.WaitAll(tasks.ToArray());
            Console.WriteLine("Time ellapsed(ms): " + myStopWatch.ElapsedMilliseconds);
            myStopWatch.Stop();
        }
    }
    class Client
    {
        public static IEnumerable<Client> GetClients()
        {
            for (int i = 0; i < 100; i++)
            {
                yield return new Client() { Id = Guid.NewGuid() };
            }
        }

        public Guid Id { get; set; }

        //This method has to be called before you use a client
        //For the sample, I don't put it on the constructor
        public async Task Initialize()
        {
            await Task.Factory.StartNew(() =>
                                      {
                                          Stopwatch timer = new Stopwatch();
                                          timer.Start();
                                          while(timer.ElapsedMilliseconds<1000)
                                          {}
                                          timer.Stop();

                                      });
            Console.WriteLine("Completed: " + Id);
        }
    }
}

他们完成任务所用的时间是多少? - matthewr
RunCode1() 耗时 23244ms,而 RunCode2() 耗时 23219ms。我有 4 个核心,所以说实话这比我预期的要快一点,但是两者之间的时间差异相当微不足道。 - Ross Dargan
它们非常接近,但我认为如果您在更多客户端(数千个)上尝试或在“Initialize”中添加较长的延迟,您将开始看到差异。(使用Thread.Sleep(time);代替所有那些秒表代码) - matthewr
1
有趣的是,这对时间产生了巨大的影响。现在 RunCode1() 需要 12016 毫秒,而 RunCode2() 需要 6019 毫秒。 - Ross Dargan
使用并行处理时,您主要会在处理大量数据时看到差异,除此之外,您不会看到太多变化。 - matthewr
3
可能是Nesting await in Parallel.ForEach的重复问题。 - Vitaliy Ulantikov
3个回答

30

应该几乎没有明显的区别。

在你的第一种情况中:

var tasks = Client.GetClients().Select(async p => await p.Initialize());

执行线程将(逐个)开始执行客户端列表中每个元素的InitializeInitialize立即将一个方法排队到线程池并返回未完成的Task

在您的第二种情况中:

var tasks = Client.GetClients().AsParallel().Select(async p => await p.Initialize());
执行线程会在线程池中创建新线程,并行地为客户端列表中的每个元素开始执行InitializeInitialize的行为相同:它会立即将一个方法排队到线程池并返回。
两个时间几乎相同,因为您只并行化了一小部分代码:将方法排队到线程池和返回未完成的Task
如果Initialize在第一个await之前执行一些较长的(同步)工作,则使用AsParallel可能是有意义的。
请记住,所有的async方法(和lambda表达式)最初都是同步执行的(请参见官方FAQ我的介绍帖子)。

7

有一个显著的主要区别。

在下面的代码中,你需要自己执行分区。换句话说,你需要为从调用GetClients()返回的IEnumerable<T>中的每个项创建一个Task对象:

var tasks= Client.GetClients().Select(async p => await p.Initialize());

在第二种情况下,对AsParallel的调用将在内部使用Task实例来执行IEnumerable<T>的分区,同时您将获得从lambda async p => await p.Initialize()返回的初始Task
var tasks = Client.GetClients().AsParallel().
    Select(async p => await p.Initialize());

最后,使用async/await并没有真正意义上的提升效率。尽管编译器可能会优化掉这个操作,但你只是等待返回一个Task的方法,然后通过lambda表达式返回一个不做任何事情的继续执行任务。既然调用Initialize函数已经返回了一个Task,那么最好保持简单,只需执行:

var tasks = Client.GetClients().Select(p => p.Initialize());

这将为您返回Task实例的序列。


-1
为了改进以上两个答案,这是最简单的方法来获得一个可等待的异步/线程执行:
var results = await Task.WhenAll(Client.GetClients()
                        .Select(async p => p.Initialize()));

这将确保它会启动单独的线程,并且您可以在最后获得结果。希望能帮到别人。花了我相当长的时间才弄清楚这一点,因为这很不显然,并且AsParallel()函数似乎是你想要的,但它不使用async/await


问题中的代码已经开始一系列任务,然后在最后等待它们全部完成。此外,这段代码(以及问题的两个解决方案之一)根本没有使用任何额外的线程。并行执行异步操作不涉及任何额外的线程(除非这些特定的异步操作恰好是使用额外的线程实现的,但许多操作并不会这样)。 - Servy
我理解你的意思,但关键是异步和使用原始的forall会导致错误。 - James Hancock
如果你的观点是OP的代码不起作用,那么你应该在你的答案中说出来,因为目前你的答案没有这样的说法。你的答案只是建议OP做他们已经在做的事情,这并不是一个有成效的答案。当然,你没有说它不起作用是好事,因为这不是真的(至少在这种情况下,你可以构造使用AsParallel会破坏代码的情况)。 - Servy
AsParallel(async)会破坏很多代码,因为在例如Web服务中没有等待线程完成的方法。我的解决方案会正确地启动线程(如果需要),并等待它们完成,这样你就可以在函数结束时不再有运行中的线程。这就是我回答的重点。 - James Hancock
问题中的代码演示了如何获取返回的任务并等待它们,因此不,它不会等待它们。另外,正如提到的那样,你的代码根本没有创建任何额外的线程,这意味着你的答案中的代码不仅重复了问题中的内容,而且你对它的解释是明显错误的。 - Servy
1
非常感谢,有用的评论,不知道为什么会被踩。解决了我的问题,AsParallel() 没有起作用。 - Rimcanfly

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接