Disclaimer: the methods and code examples in this article are the fruit of my own investigation and self learning, and in no means these are ready to be used in a real, production environment. Use at your own risk. To learn more about parallel and synchronous programming, I recommend reading the official documentation at: https://docs.microsoft.com/dotnet/standard/parallel-processing-and-concurrency 

This post is part of a series of 3 articles about performace improvement with parallel programming:

1. Improve the performance with asynchronous functions to run processes in parallel
2. Make use of ConcurrentBag to store the results from asynchronous processes
3. Parallel programming in C# with class Parallel (this article)

 

Following up with the series of posts about async programming and performance improvements using Tasks for process parallelization, in this one I am introducing a new option, using Parallel class available in .Net, and I will compare it with the previous examples, where we run a process that returns a list of 10 random numbers between 1 and 100, and we cut it down to 0,6s execution time, from the starting 2s of the synchronous process:

Result: 82,83,88,99,57,71,13,54,18,40
Completed: 00:00:00.6276547

You can find the source code of this examples on Github: https://github.com/sgisbert/parallelization

At this examples, we used Task.Run() to execute a process in a new thread. It is important to understand that the use of Task to create new threads is limited to the number of cores available, and the excesive use of these can lead to a not so efficient use of system resources. To better understand these concepts, I recommend further reading: https://www.pluralsight.com/guides/using-task-run-async-await#module-knowthenatureofyourasynchronousoperation

The use of async/await pattern is specially recommended with libraries or API calls that already offer async methods, without the need to use Task.Run() to create them. Good examples of this are HttpClient or FileStream, that offer I/O async methods and we can take full benefit of async programming.

If still we have a use case like in the example, where you need to execute the same logic over several objects, we can use the class Parallel, and see how it performs, in comparison with Task.WaitAll() or Task.WhenAll().

 

Task.WaitAll() vs Task.WhenAll()

There are two different ways to wait for all our tasks to complete:

await Task.WhenAll(tasks.ToArray());

and 

Task.WaitAll(tasks.ToArray());

and it is interesting to know the differences between both. In terms of performance, the process takes a very similar amount of time to complete: 0,6s, but there are some aspects worth to be noted:

Task.WaitAll() 

  • The main UI execution thread is blocked until all the tasks are completed, so it does not return a Task. This means that if we have a UI dependant application, it could freeze until the execution ends.
  • If there are any exceptions thrown, it will throw an AggregateException with all the exceptions together.

await Task.WhenAll()

  • The main thread is not blocked, so this would be the recommended approach for a UI application and async methods, as it actually returns a Task.
  • If there are any exceptions, only the first one is returned.

In this article, you can explore deeper the differences between both methods: https://www.infoworld.com/article/3152735/when-to-use-taskwaitall-vs-taskwhenall-in-net.html

 

Using Parallel class

Another way to make use of parallel programming advantages, is with class Parallel in .Net (more info in the official documentation)

First thing to do with our code will be to transform the process so it does not need to use Task.Run(), but keeping the compatibility with Task to be able to compare both results. So, let's modify the method in the following way (see full source code on GitHub):

private static async Task Process(int id, ConcurrentBag<int> cb)
{
    await Task.Run(() =>
    {
        CoreProcess(id, cb);
    });
}

private static void CoreProcess(int id, ConcurrentBag<int> cb)
{
    Stopwatch timer = new Stopwatch();
    timer.Start();
    Random random = new Random();
    Thread.Sleep(200);

    int number = random.Next(1, 100);
    cb.Add(number);

    Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Process {id}: {timer.Elapsed}");
}

We also added the value of Thread.CurrentThread.ManagedThreadId to the console output to check the Threads being created.

This way, we can call our process both in sync or async way.

Parallel.ForEach()

Now we can add the following code, using Parallel.ForEach() to run the synchronous process, but being parallelized (see full source code on GitHub):

timer.Restart();
ConcurrentBag<int> cb2 = new ConcurrentBag<int>();
var sourceCollection = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
Parallel.ForEach(sourceCollection, (id) =>
{
    CoreProcess(id, cb2);
});
Console.WriteLine($"Result: {string.Join(",", cb2)}");
Console.WriteLine($"Completed: {timer.Elapsed}");
Console.WriteLine();

And let's compare the results:

Task.WaitAll()

[6] Process 2: 00:00:00.2028313
[5] Process 1: 00:00:00.2039001
[4] Process 0: 00:00:00.2063105
[7] Process 3: 00:00:00.2027921
[4] Process 5: 00:00:00.2006033
[7] Process 7: 00:00:00.2006397
[5] Process 6: 00:00:00.2006732
[6] Process 4: 00:00:00.2007073
[7] Process 8: 00:00:00.2001104
[5] Process 9: 00:00:00.2009985

Result: 15,49,52,76,90,1,71,67,37,23
Completed: 00:00:00.6398704

Parallel.ForEach()

[6] Process 9: 00:00:00.2011420
[5] Process 3: 00:00:00.2012138
[1] Process 1: 00:00:00.2013376
[4] Process 7: 00:00:00.2011324
[7] Process 5: 00:00:00.2011974
[1] Process 2: 00:00:00.2004285
[4] Process 8: 00:00:00.2004063
[7] Process 6: 00:00:00.2005255
[6] Process 10: 00:00:00.2006825
[5] Process 4: 00:00:00.2006454
Result: 50,82,38,39,37,96,80,48,95,90
Completed: 00:00:00.4507518

The task run with Parallel has decreased the time to 0,45s in front of the 0,6s of Task. We got a performance improvement without having to make use of Task.Run(), which could bring up potential problems with the use of system resources.

Parallel.Invoke()

Parallel offers also another alternative to execute parallel tasks, with method Invoke(). To test it, let's add the following code lines (see full source code on Github):

timer.Restart();
ConcurrentBag<int> cb3 = new ConcurrentBag<int>();
List<Action> actions = new List<Action>();
foreach (var i in sourceCollection)
{
    actions.Add(() => CoreProcess(i, cb3));
}
Parallel.Invoke(actions.ToArray());
Console.WriteLine($"Result: {string.Join(",", cb3)}");
Console.WriteLine($"Completed: {timer.Elapsed}");

With the following results:

[4] Process 3: 00:00:00.2008078
[6] Process 4: 00:00:00.2008435
[5] Process 5: 00:00:00.2007954
[7] Process 2: 00:00:00.2008161
[1] Process 1: 00:00:00.2009441
[7] Process 8: 00:00:00.2001254
[5] Process 6: 00:00:00.2001896
[4] Process 7: 00:00:00.2001298
[8] Process 10: 00:00:00.2033235
[6] Process 9: 00:00:00.2089627
Result: 55,26,74,29,32,45,71,46,39,90
Completed: 00:00:00.4121676

In this case, Invoke() reduces the time up to nearly 0,4s.

 

Conclusions

  • Analize deeply each situation, understanding how async methods work and the differences between Task.WaitAll y Task.WhenAll, to make use of them correctly on every situation.
  • Know other alternatives like Parallel to improve the application performance.
  • Don't take any of these examples for granted. Make your own performance tests and check on every case what solution works best for you