233

What are the differences between using Parallel.ForEach or Task.Run() to start a set of tasks asynchronously?

Version 1:

List<string> strings = new List<string> { "s1", "s2", "s3" };
Parallel.ForEach(strings, s =>
{
    DoSomething(s);
});

Version 2:

List<string> strings = new List<string> { "s1", "s2", "s3" };
List<Task> Tasks = new List<Task>();
foreach (var s in strings)
{
    Tasks.Add(Task.Run(() => DoSomething(s)));
}
await Task.WhenAll(Tasks);
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Petter T
  • 3,387
  • 2
  • 19
  • 31
  • 5
    I think the 2nd code fragment would be almost equal to the 1st one if you used `Task.WaitAll` instead of `Task.WhenAll`. – avo Oct 01 '13 at 05:41
  • 20
    Please also note that the second one will perform DoSomething("s3") three times and it will not produce the same result! http://stackoverflow.com/questions/4684320/starting-tasks-in-foreach-loop-uses-value-of-last-item – Nullius Nov 14 '14 at 16:09
  • 1
    Possible duplicate of [Parallel.ForEach vs Task.Factory.StartNew](http://stackoverflow.com/questions/5009181/parallel-foreach-vs-task-factory-startnew) – Mohammad Feb 16 '16 at 15:59
  • @Dan: note that Version 2 uses async/await, which means it's a different question. Async/await was introduced with VS 2012, 1.5 years after the possible duplicate thread was written. – Petter T Feb 17 '16 at 07:47
  • Might take a look at [.net Core Parallel.ForEach issues](https://stackoverflow.com/a/39796934) – Heretic Monkey Jun 20 '18 at 21:10
  • 7
    @Nullius, since C#5, captured variables behave the expected way, and the loop above performs DoSomething for each of the three strings, see e.g. https://stackoverflow.com/questions/12112881/has-foreachs-use-of-variables-been-changed-in-c-sharp-5. This question is obviously for C#5, as Task.WhenAll was introduced in C#5, with .NET Framework 4.5. So it is not correct that the second one will perform DoSomething("s3") three times. – Petter T Jun 28 '21 at 12:27
  • .NET version != language version. But nice addition regardless. My previous comment is probably less relevant today but imo still was nearly 7 years ago. – Nullius Jun 29 '21 at 13:29
  • I am aware. I am talking about language version, C#5. – Petter T Jun 30 '21 at 16:04

4 Answers4

239

In this case, the second method will asynchronously wait for the tasks to complete instead of blocking.

However, there is a disadvantage to use Task.Run in a loop- With Parallel.ForEach, there is a Partitioner which gets created to avoid making more tasks than necessary. Task.Run will always make a single task per item (since you're doing this), but the Parallel class batches work so you create fewer tasks than total work items. This can provide significantly better overall performance, especially if the loop body has a small amount of work per item.

If this is the case, you can combine both options by writing:

await Task.Run(() => Parallel.ForEach(strings, s =>
{
    DoSomething(s);
}));

Note that this can also be written in this shorter form:

await Task.Run(() => Parallel.ForEach(strings, DoSomething));
Reed Copsey
  • 554,122
  • 78
  • 1,158
  • 1,373
  • 3
    Great answer, I was wondering if you could point me to a good reading material on this topic ? – Dimitar Dimitrov Apr 23 '14 at 09:03
  • @DimitarDimitrov For general TPL stuff, http://reedcopsey.com/series/parallelism-in-net4/ – Reed Copsey Apr 23 '14 at 18:07
  • 4
    My Parallel.ForEach construct was crashing my application. I was performing some heavy image processing inside it. However, when i added Task.Run(()=> Parallel.ForEach(....)); It stopped crashing. Can you explain why? Please note i constrain the parallel options to the number of cores on system. – monkeyjumps Nov 06 '14 at 18:33
  • 1
    @MonkeyJumps Because `Parallel.ForEach` will block the current thread while `Task.Run` uses the async framework to allow the thread to continue running without blocking. – Josh M. Aug 09 '15 at 23:12
  • 4
    What if `DoSomething` is `async void DoSomething`? – Francesco Bonizzi Feb 19 '16 at 10:50
  • 1
    What about `async Task DoSomething`? – Shawn Mclean Jun 27 '17 at 20:09
  • 1
    @ShawnMclean - you can add async as:await Task.Run(() => Parallel.ForEach(strings, async s => { await DoSomething(s); })); – ScottFoster1000 Nov 06 '18 at 21:24
  • When you call `Parallel.ForEach` the **calling thread won't be blocked**. Richter says: All of `Parallel`’s methods have the calling thread **participate** in the processing of the work, which is good in terms of resource usage because we wouldn’t want the calling thread to just suspend itself while waiting for thread pool threads to do all the work. – Boris Makhlin Jun 25 '21 at 14:08
  • 1
    @BorisMakhlin Yes, technically the thread is working, but the effect from the developer's perspective is a blocking call. – Reed Copsey Jul 05 '21 at 20:48
  • Assuming async await concept is added to verison 1, thus making it unblock the ui thread. Then is there any difference between the overload on the computer between version 1 and version 2? I'm trying to understand whether the choice makes any difference in terms of computer performance for large number of iteration. – variable Aug 21 '21 at 03:40
  • 1
    if you have `async Task DoSomething` then you would just change `s` to `async s` and `DoSomething(s);` to `await DoSomething(s);` @FrancescoBonizzi have you been awaiting for that for a long time ;-) – Dan Ciborowski - MSFT Aug 11 '23 at 02:12
51

The first version will synchronously block the calling thread (and run some of the tasks on it).
If it's a UI thread, this will freeze the UI.

The second version will run the tasks asynchronously in the thread pool and release the calling thread until they're done.

There are also differences in the scheduling algorithms used.

Note that your second example can be shortened to

await Task.WhenAll(strings.Select(s => Task.Run(() => DoSomething(s))));
Rand Random
  • 7,300
  • 10
  • 40
  • 88
SLaks
  • 868,454
  • 176
  • 1,908
  • 1,964
  • 3
    shouldn't it be `await Task.WhenAll(strings.Select(async s => await Task.Run(() => DoSomething(s)));`? I had problems when returning tasks (instead of awaiting) especially when statements like `using` were involved to dispose objects. – Martín Coll Oct 08 '14 at 15:19
  • My Parallel.ForEach call was causing my UI to crash I added Task.Run(()=> Parallel.ForEach (.... ) ); to it and it resolved crashing. – monkeyjumps Nov 06 '14 at 18:40
  • Does option 2 add extra overhead to the computer compared to option 1 for large number of tasks? – variable Aug 21 '21 at 03:37
1

I have seen Parallel.ForEach used inappropriately, and I figured an example in this question would help.

When you run the code below in a Console app, you will see how the tasks executed in Parallel.ForEach doesn't block the calling thread. This could be okay if you don't care about the result (positive or negative) but if you do need the result, you should make sure to use Task.WhenAll.

using System;
using System.Linq;
using System.Threading.Tasks;

namespace ParrellelEachExample
{
    class Program
    {
        static void Main(string[] args)
        {
            var indexes = new int[] { 1, 2, 3 };

            RunExample((prefix) => Parallel.ForEach(indexes, (i) => DoSomethingAsync(i, prefix)),
                "Parallel.Foreach");

            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("*You'll notice the tasks haven't run yet, because the main thread was not blocked*");
            Console.WriteLine("Press any key to start the next example...");
            Console.ReadKey();
            
            RunExample((prefix) => Task.WhenAll(indexes.Select(i => DoSomethingAsync(i, prefix)).ToArray()).Wait(),
                "Task.WhenAll");
            Console.WriteLine("All tasks are done.  Press any key to close...");
            Console.ReadKey();
        }

        static void RunExample(Action<string> action, string prefix)
        {
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine($"{Environment.NewLine}Starting '{prefix}'...");
            action(prefix);
            Console.WriteLine($"{Environment.NewLine}Finished '{prefix}'{Environment.NewLine}");
        }
        

        static async Task DoSomethingAsync(int i, string prefix)
        {
            await Task.Delay(i * 1000);
            Console.WriteLine($"Finished: {prefix}[{i}]");
        }
    }
}

Here is the result:

enter image description here

Conclusion:

Using the Parallel.ForEach with a Task will not block the calling thread. If you care about the result, make sure to await the tasks.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Rogala
  • 2,679
  • 25
  • 27
  • 1
    I think this result is obvious, cause you start Async Method from ForEach Body (i.e. using new ThreadPool Thread without waiting for result). Here we must call DoSomethingAsync(i, prefix).Result. – Mic Jul 09 '21 at 11:11
  • @Mic While the result may seem obvious for you, the result of using Parallel.ForEach inappropriately in a web application can cause some serious issues within a server that will not appear until a load is put on the app. The post is not to say you shouldn't use it, but to make sure those who do use it know what is actually going to happen. Additionally, you should avoid using .Result as you should always be using async/await. – Rogala Jul 17 '21 at 16:33
  • 1
    `Parallel.ForEach` cannot be used for async method calls. As `DoSomething` returns a Task which is not awaited, you should call `.Wait()` on it. Now you'll see that `Parallel.ForEach` returns only after all work is done. – Bouke Aug 27 '21 at 08:37
  • @Bouke the point of the answer is to help those that are not aware of the differences. That said, you can use a task within Parallel.ForEach, but it will not be executed on the main thread. That does not mean you should, but it is allowable in the code as the example demonstrates. This means the code in the task is executing on a different thread and not blocked. There are scenarios where someone may want to have this occur, but they should be aware of what is happening. – Rogala Aug 27 '21 at 17:34
  • Using the `Parallel.ForEach` with `async` delegate [is a bug](https://stackoverflow.com/questions/23137393/parallel-foreach-and-async-await "Parallel.ForEach and async-await"). The resulting behavior is never desirable. The correct API for parallelizing asynchronous work is the [`Parallel.ForEachAsync`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.parallel.foreachasync). The question makes no mention that the underlying work can be asynchronous, so this answer is off topic, hence my downvote. – Theodor Zoulias Jan 19 '23 at 05:24
  • @AndrewD.Bond regarding your edit ([revision 3](https://stackoverflow.com/revisions/57085704/3)), using the `Parallel.ForEach` with `async` delegate is not the same as calling an asynchronous method without awaiting it. The former is `async void` ([fire-and-crash](https://stackoverflow.com/questions/17659603/async-void-asp-net-and-count-of-outstanding-operations/17660475#17660475)), and the later is [fire-and-forget](https://stackoverflow.com/questions/61316504/proper-way-to-start-and-async-fire-and-forget-call/61320933#61320933). `async void` delegates are not tasks. – Theodor Zoulias Jan 19 '23 at 05:28
0

I ended up doing this, as it felt easier to read:

  List<Task> x = new List<Task>();
  foreach(var s in myCollectionOfObject)
  {
      // Note there is no await here. Just collection the Tasks
      x.Add(s.DoSomethingAsync());
  }
  await Task.WhenAll(x);
Chris M.
  • 1,731
  • 14
  • 15
  • This way you are doing the Tasks are being executed one after the other or the WhenAll are starting all of them at once? – Vinicius Gualberto Apr 24 '18 at 01:36
  • 1
    As far as I can tell, they're all started when I call "DoSomethingAsync()". Nothing is blocking on them, however, until the WhenAll is called. – Chris M. Apr 25 '18 at 04:47
  • You mean when the first "DoSomethingAsync()" is called? – Vinicius Gualberto Apr 25 '18 at 05:23
  • 3
    @ChrisM. It will be blocked until the first await of DoSomethingAsync() since this is what will transfer execution back to your loop. If it's synchronous and your return a Task, all of the code will be run one after another and the WhenAll will wait for all the Tasks to complete – Simon Belanger Nov 09 '18 at 16:37