Task parallelism is the process of running tasks in parallel. Task parallelism divides tasks and allocates those tasks to separate threads for processing. It is based on unstructured parallelism. It means the parallel work unit may start and finish in places scattered according to the execution of the program.

In this article, we will be discussing different methods to achieve parallelism using Tasks in C#.

Note: If you are not familiar with Tasks in C#, you can learn more about it in my blog post C# Task.

Content

Attaching Child Tasks To A Parent Task

A Task can have several child Tasks. The parent Task finishes when all the child tasks are ready. Below code shows how this works.

1 using System ; 2 using System.Threading ; 3 using System.Threading.Tasks ; 4 5 class Program 6 { 7 static void Main ( string [] args ) 8 { 9 Task < int []> parent = new Task < int []>(() => 10 { 11 var results = new int [ 3 ]; 12 new Task (() => { 13 Thread . Sleep ( 15000 ); 14 results [ 0 ] = 0 ; 15 }, 16 TaskCreationOptions . AttachedToParent ). Start (); 17 new Task (() => results [ 1 ] = 1 , 18 TaskCreationOptions . AttachedToParent ). Start (); 19 new Task (() => results [ 2 ] = 2 , 20 TaskCreationOptions . AttachedToParent ). Start (); 21 return results ; 22 }); 23 parent . Start (); 24 var finalTask = parent . ContinueWith ( 25 parentTask => { 26 foreach ( int i in parentTask . Result ) 27 Console . WriteLine ( i ); 28 }); 29 finalTask . Wait (); 30 Console . ReadLine (); 31 } 32 }

The finalTask runs only after the parent Task is finished, and the parent Task finishes when all three children are finished. You can use this to create quite complex Task hierarchies that will go through all the steps you specified.

Note: Task.Run explicitly forbids having child tasks. Task.Run is the equivalent of Task.Factory.StartNew with the default scheduler and most importantly, TaskCreationOptions.DenyChildAttach. So any child task that attempts to execute as an attached child task (that is, it is created with the AttachedToParent option) will not be able to attach to the parent task and will execute instead as a detached child task.

C# TaskFactory

In the previous example, you had to create three Tasks all with the same options. To make the process easier, you can use a TaskFactory. A TaskFactory is created with a certain configuration and can then be used to create Tasks with that configuration. Below code shows how you can simplify the previous example with a factory.

1 using System ; 2 using System.Threading ; 3 using System.Threading.Tasks ; 4 5 class Program 6 { 7 static void Main ( string [] args ) 8 { 9 Task < int []> parent = new Task < int []>(() => 10 { 11 var results = new int [ 3 ]; 12 TaskFactory tf = new TaskFactory ( TaskCreationOptions . AttachedToParent , 13 TaskContinuationOptions . ExecuteSynchronously ); 14 tf . StartNew (() => { 15 Thread . Sleep ( 15000 ); 16 results [ 0 ] = 0 ; 17 }); 18 tf . StartNew (() => results [ 1 ] = 1 ); 19 tf . StartNew (() => results [ 2 ] = 2 ); 20 return results ; 21 }); 22 parent . Start (); 23 var finalTask = parent . ContinueWith ( 24 parentTask => { 25 foreach ( int i in parentTask . Result ) 26 Console . WriteLine ( i ); 27 }); 28 finalTask . Wait (); 29 Console . ReadLine (); 30 } 31 }

C# Task.WaitAll

You can also use the method WaitAll to wait for multiple Tasks to finish before continuing execution. These are similar to Task.Wait, except they wait for multiple tasks to all complete.

Below code shows how to use this.

1 using System ; 2 using System.Threading ; 3 using System.Threading.Tasks ; 4 5 namespace Program_7 6 { 7 class Program 8 { 9 static void Main ( string [] args ) 10 { 11 Task [] tasks = new Task [ 3 ]; 12 tasks [ 0 ] = Task . Run (() => { 13 Thread . Sleep ( 1000 ); 14 Console . WriteLine ( "1" ); 15 return 1 ; 16 }); 17 tasks [ 1 ] = Task . Run (() => { 18 Thread . Sleep ( 1000 ); 19 Console . WriteLine ( "2" ); 20 return 2 ; 21 }); 22 tasks [ 2 ] = Task . Run (() => { 23 Thread . Sleep ( 1000 ); 24 Console . WriteLine ( "3" ); 25 return 3 ; 26 } 27 ); 28 Task . WaitAll ( tasks ); 29 Console . ReadLine (); 30 } 31 } 32 }

In this case, all three Tasks are executed simultaneously, and the whole run takes approximately 1000ms instead of 3000. Next to WaitAll, you also have a WhenAll method that you can use to schedule a continuation method after all Tasks have finished.

Task.WaitAll should be very rarely used. It is occasionally useful when working with Delegate Tasks, but even this usage is rare. Developers writing parallel code should first attempt data parallelism; and even if task parallism is necessary, then parent/child tasks may result in cleaner code than Task.WaitAll method.

C# Task.WaitAny

You can also use the WaitAny method to wait until one of the tasks is finished.

We can use Task.WaitAny when we have a collection of tasks, but we only interested in the first finished task. It can happen for example when we have a couple of async API that all of them do the same thing. But we want to receive the result from the one that returns the result first.

Also, you can create a WaitAllOneByOne pattern using the WaitAny method. It is useful when we would like to wait till all tasks finish, but process results as each one complete.

Below code shows how this works.

1 using System ; 2 using System.Linq ; 3 using System.Threading ; 4 using System.Threading.Tasks ; 5 namespace Chapter1 6 { 7 public static class Program 8 { 9 public static void Main () 10 { 11 Task < int >[] tasks = new Task < int >[ 3 ]; 12 tasks [ 0 ] = Task . Run (() => { Thread . Sleep ( 2000 ); return 1 ; }); 13 tasks [ 1 ] = Task . Run (() => { Thread . Sleep ( 1000 ); return 2 ; }); 14 tasks [ 2 ] = Task . Run (() => { Thread . Sleep ( 3000 ); return 3 ; }); 15 while ( tasks . Length > 0 ) 16 { 17 int i = Task . WaitAny ( tasks ); 18 Task < int > completedTask = tasks [ i ]; 19 Console . WriteLine ( completedTask . Result ); 20 var temp = tasks . ToList (); 21 temp . RemoveAt ( i ); 22 tasks = temp . ToArray (); 23 } 24 Console . ReadLine (); 25 } 26 } 27 }

In this example, you process a completed Task as soon as it finishes. By keeping track of which Tasks are finished, you don’t have to wait until all Tasks have completed.

C# Parallel Class

The System.Threading.Tasks namespace also contains another class that can be used for parallel processing. The Parallel class has a couple of static methods—For, ForEach, and Invoke—that you can use to parallelize work.

Parallelism involves taking a certain task and splitting it into a set of related tasks that can be executed concurrently. This also means that you shouldn’t go through your code to replace all your loops with parallel loops. You should use the Parallel class only when your code doesn’t have to be executed sequentially.

Increasing performance with parallel processing happens only when you have a lot of work to be done that can be executed in parallel. For smaller work sets or for work that has to synchronize access to resources, using the Parallel class can hurt performance.

The best way to know whether it will work in your situation is to measure the results.

Below code shows an example of using Parallel.For and Parallel.ForEach.

C# Parallel.For and Parallel.Foreach

1 using System.Linq ; 2 using System.Threading ; 3 using System.Threading.Tasks ; 4 5 namespace Example9 6 { 7 class Program 8 { 9 static void Main ( string [] args ) 10 { 11 Parallel . For ( 0 , 10 , i => 12 { 13 Thread . Sleep ( 1000 ); 14 }); 15 var numbers = Enumerable . Range ( 0 , 10 ); 16 Parallel . ForEach ( numbers , i => 17 { 18 Thread . Sleep ( 1000 ); 19 }); 20 Console . ReadLine (); 21 } 22 } 23 }

You can cancel the loop by using the ParallelLoopState object. You have two options to do this: Break or Stop. Break ensures that all iterations that are currently running will be finished. Stop just terminates everything. Here is an example:

C# Parallel.Break

1 using System ; 2 using System.Threading.Tasks ; 3 4 namespace Example10 5 { 6 class Program 7 { 8 static void Main ( string [] args ) 9 { 10 ParallelLoopResult result = Parallel . 11 For ( 0 , 1000 , ( int i , ParallelLoopState loopState ) => 12 { 13 if ( i == 500 ) 14 { 15 Console . WriteLine ( "Breaking loop" ); 16 loopState . Break (); 17 } 18 return ; 19 }); 20 Console . ReadLine (); 21 } 22 } 23 }

When breaking the parallel loop, the result variable has an IsCompleted value of false and a LowestBreakIteration of 500. When you use the Stop method, the LowestBreakIteration is null.