25

I was looking at someone sample code for async and noticed a few issues with the way it was implemented. Whilst looking at the code I wondered if it would be more efficient to loop through a list using as parallel, rather than just looping through the list normally.

As far as I can tell there is very little difference in performance, both use up every processor, and both talk around the same amount of time to completed.

This is the first way of doing it

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

And this is the second

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

Am I correct in assuming there is no difference between the two?

The full program can be found below

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);
        }
    }
}
Ralph Willgoss
  • 11,750
  • 4
  • 64
  • 67
Ross Dargan
  • 5,876
  • 4
  • 40
  • 53
  • What was the time that they took to complete? – matthewr Sep 24 '12 at 11:54
  • RunCode1() takes 23244ms, and RunCode2() takes 23219ms. I have 4 cores so this is a little faster than I would have expected to be honest, but the times between the two are pretty insignificant I think. – Ross Dargan Sep 24 '12 at 12:03
  • They are pretty close together but I think if you try it on more clients (in the thousands) or add a longer delay in `Initialize` you will start to see a difference. (Use `Thread.Sleep(time);` instead of all that stopwatch code) – matthewr Sep 24 '12 at 12:08
  • 1
    interestingly that makes a huge difference to the times. RunCode1() now takes 12016, and RunCode2() now takes 6019. – Ross Dargan Sep 24 '12 at 12:21
  • With parallelism you will mainly see a difference when you use it with large amounts of data, apart from that the you wont see much of a change. – matthewr Sep 24 '12 at 12:30
  • 3
    Possible duplicate of [Nesting await in Parallel.ForEach](https://stackoverflow.com/questions/11564506/nesting-await-in-parallel-foreach) – Vitaliy Ulantikov Dec 02 '17 at 20:56

3 Answers3

30

There should be very little discernible difference.

In your first case:

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

The executing thread will (one at a time) start executing Initialize for each element in the client list. Initialize immediately queues a method to the thread pool and returns an uncompleted Task.

In your second case:

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

The executing thread will fork to the thread pool and (in parallel) start executing Initialize for each element in the client list. Initialize has the same behavior: it immediately queues a method to the thread pool and returns.

The two timings are nearly identical because you're only parallelizing a small amount of code: the queueing of the method to the thread pool and the return of an uncompleted Task.

If Initialize did some longer (synchronous) work before its first await, it may make sense to use AsParallel.

Remember, all async methods (and lambdas) start out being executed synchronously (see the official FAQ or my own intro post).

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
7

There's a singular major difference.

In the following code, you are taking it upon yourself to perform the partitioning. In other words, you're creating one Task object per item from the IEnumerable<T> that is returned from the call to GetClients():

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

In the second, the call to AsParallel is internally going to use Task instances to execute partitions of the IEnumerable<T> and you're going to have the initial Task that is returned from the lambda async p => await p.Initialize():

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

Finally, you're not really doing anything by using async/await here. Granted, the compiler might optimize this out, but you're just waiting on a method that returns a Task and then returning a continuation that does nothing back through the lambda. That said, since the call to Initialize is already returning a Task, it's best to keep it simple and just do:

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

Which will return the sequence of Task instances for you.

casperOne
  • 73,706
  • 19
  • 184
  • 253
-1

To improve on the above 2 answers this is the simplest way to get an async/threaded execution that is awaitable:

var results = await Task.WhenAll(Client.GetClients()
                        .Select(async p => p.Initialize()));

This will ensure that it spins separate threads and that you get the results at the end. Hope that helps someone. Took me quite a while to figure this out properly since this is very not obvious and the AsParallel() function seems to be what you want but doesn't use async/await.

Noctis
  • 11,507
  • 3
  • 43
  • 82
James Hancock
  • 3,348
  • 5
  • 34
  • 59
  • The code in the question is *already* starting a series of tasks, and then waits for them all at the end. Additionally, this code (as well as one of the questions two solutions) doesn't use any additional threads at all. Doing asynchronous operations in parallel doesn't involve any additional threads (unless those specific asynchronous operations happen to be implemented using additional threads, which many will not). – Servy May 15 '18 at 18:02
  • I get that, but the point is async and you will get errors using async with forall as originally asked. – James Hancock May 15 '18 at 18:18
  • If your point is that the OP's code doesn't work then you should say that in your answer, because currently your answer says no such thing. Your answer just suggests that the OP do *the thing that they're already doing*, which isn't a productive answer. it's of course a good thing that you haven't said that it doesn't work, because it's not true (at least in this case, you can construct situations in which using `AsParallel` would break the code). – Servy May 15 '18 at 18:20
  • AsParallel(async) will break lots of code because there is no way to wait for the threads to complete in say a web service as an example. My solution spins threads (if needed) and awaits them correctly so that you're leaving the function without the threads still running. Hence the point to my answer. – James Hancock May 15 '18 at 18:22
  • The code in the question *demonstrates* how to get the returned tasks and wait for them, so no, it doesn't not wait for them. Additionally, as mentioned, your code does not create any additional threads at all, meaning that not only is the code in your answer just repeating what's in the question, but your explanation of it is demonstrably wrong. – Servy May 15 '18 at 18:36
  • 1
    Thanks a lot, helpful comment, don't know why it's downvoted. Solved my issue, AsParallel() didn't work. – Rimcanfly Dec 18 '19 at 12:25