Shadman Kudchikar

C# Task

Content

What Is A Task In C# ?

Task is one of the central elements of the task-based asynchronous pattern first introduced in the .NET Framework 4.

Task object typically executes asynchronously on a thread pool thread rather than synchronously on the main application thread.

As we learned in the previous chapter we can offload the work to thread pool using the queue user work item method. However, this method has its weaknesses as we can’t tell whether the operation has finished or what a return value is.

This is where a Task can be helpful. The Task can tell you if the work is completed and if the operation returns a result. A Task is an object that represents some work that should be done.

A task scheduler is responsible for starting the Task and managing it. By default, the Task scheduler uses threads from the thread pool to execute the Task.

Why Use A Task In C# ?

  • Tasks can be used to make your application more responsive. If the thread that manages the user interface offloads work to another thread from the thread pool, it can keep processing user events and ensure that the application can still be used.

  • You can also parallelize your CPU bound operation on to multiple processors using task.

Starting A New Task

using System;
using System.Threading.Tasks;

namespace CSharp_Task
{
    public class Program
    {
        static void Main(string[] args)
        {
            Task t = Task.Run(() =>
            {
                for (int x = 0; x < 100; x++)
                {
                    Console.Write('*');
                }
            });
            t.Wait();

            Console.WriteLine();
            Console.WriteLine("Press Enter to terminate!");
            Console.ReadLine();
        }
    }
}

This example creates a new Task and immediately starts it. Calling Wait is equivalent to calling Join on a thread. It waits until the Task is finished before exiting the application.

C# Task That Returns A Value

The .NET Framework also has the generic version of task Task<T> class that you can use if a Task should return a value. Here T is the data type you want to return as a result. Below code shows how this works.

using System;
using System.Threading.Tasks;

namespace Example2
{
    class Program
    {
        static void Main(string[] args)
        {
            Task<int> t = Task.Run(() =>
            {
                return 32;
            });
            Console.WriteLine(t.Result); // Displays 32

            Console.WriteLine();
            Console.WriteLine("Press Enter to terminate!");
            Console.ReadLine();
        }
    }
}

Attempting to read the Result property on a Task will force the thread that’s trying to read the result to wait until the Task is finished before continuing. As long as the Task has not finished, it is impossible to give the result. If the Task is not finished, this call will block the current thread.

Adding A Continuation

Another great feature that task supports is the continuation. This means that you can execute another task as soon as the first Task finishes.

Below is an example of creating such a continuation.

using System;
using System.Threading.Tasks;

namespace Example3
{
    class Program
    {
        static void Main(string[] args)
        {
            Task<int> t = Task.Run(() =>
            {
                return 32;
            }).ContinueWith((i) =>
            {
                return i.Result * 2;
            });

            Console.WriteLine(t.Result);

            Console.WriteLine();
            Console.WriteLine("Press Enter to terminate!");
            Console.ReadLine();
        }
    }
}

Scheduling Different Continuation Tasks

The ContinueWith method has a couple of overloads that you can use to configure when the continuation will run. This way you can add different continuation methods that will run when an exception happens, the Task is canceled, or the Task completes successfully. Below code shows how to do this.

using System;
using System.Threading.Tasks;

namespace Example_4
{
    class Program
    {
        static void Main(string[] args)
        {
            Task<int> t = Task.Run(() =>
            {
                return 32;
            });
            t.ContinueWith((i) =>
            {
                Console.WriteLine("Canceled");
            }, TaskContinuationOptions.OnlyOnCanceled);
            t.ContinueWith((i) =>
            {
                Console.WriteLine("Faulted");
            }, TaskContinuationOptions.OnlyOnFaulted);
            var completedTask = t.ContinueWith((i) =>
            {
                Console.WriteLine("Completed");
            }, TaskContinuationOptions.OnlyOnRanToCompletion);
            completedTask.Wait();
        }
    }
}

Attaching Child Tasks To A Parent Task

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

using System;
using System.Threading.Tasks;

namespace Example_5
{
    class Program
    {
        static void Main(string[] args)
        {
            Task<Int32[]> parent = Task.Run(() =>
            {
                var results = new Int32[3];
                new Task(() => results[0] = 0,
                TaskCreationOptions.AttachedToParent).Start();
                new Task(() => results[1] = 1,
                TaskCreationOptions.AttachedToParent).Start();
                new Task(() => results[2] = 2,
                TaskCreationOptions.AttachedToParent).Start();
                return results;
            });
            var finalTask = parent.ContinueWith(
            parentTask => {
                foreach (int i in parentTask.Result)
                    Console.WriteLine(i);
            });
            finalTask.Wait();
        }
    }
}

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.

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.

using System;
using System.Threading.Tasks;

namespace Example6
{
    class Program
    {
        static void Main(string[] args)
        {
            Task<Int32[]> parent = Task.Run(() =>
            {
                var results = new Int32[3];
                TaskFactory tf = new TaskFactory(
                    TaskCreationOptions.AttachedToParent,
                TaskContinuationOptions.ExecuteSynchronously);
                tf.StartNew(() => results[0] = 0);
                tf.StartNew(() => results[1] = 1);
                tf.StartNew(() => results[2] = 2);
                return results;
            });
            var finalTask = parent.ContinueWith(
            parentTask => {
                foreach (int i in parentTask.Result)
                    Console.WriteLine(i);
            });
            finalTask.Wait();
        }
    }
}

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.

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

namespace Program_7
{
    class Program
    {
        static void Main(string[] args)
        {
            Task[] tasks = new Task[3];
            tasks[0] = Task.Run(() => {
                Thread.Sleep(1000);
                Console.WriteLine("1");
                return 1;
            });
            tasks[1] = Task.Run(() => {
                Thread.Sleep(1000);
                Console.WriteLine("2");
                return 2;
            });
            tasks[2] = Task.Run(() => {
                Thread.Sleep(1000);
                Console.WriteLine("3");
                return 3;
            }
            );
            Task.WaitAll(tasks);
        }
    }
}

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.

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Chapter1
{
    public static class Program
    {
        public static void Main()
        {
            Task<int>[] tasks = new Task<int>[3];
            tasks[0] = Task.Run(() => { Thread.Sleep(2000); return 1; });
            tasks[1] = Task.Run(() => { Thread.Sleep(1000); return 2; });
            tasks[2] = Task.Run(() => { Thread.Sleep(3000); return 3; });
            while (tasks.Length > 0)
            {
                int i = Task.WaitAny(tasks);
                Task<int> completedTask = tasks[i];
                Console.WriteLine(completedTask.Result);
                var temp = tasks.ToList();
                temp.RemoveAt(i);
                tasks = temp.ToArray();
            }
        }
    }
}

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

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

namespace Example9
{
    class Program
    {
        static void Main(string[] args)
        {
            Parallel.For(0, 10, i =>
            {
                Thread.Sleep(1000);
            });
            var numbers = Enumerable.Range(0, 10);
            Parallel.ForEach(numbers, i =>
            {
                Thread.Sleep(1000);
            });
        }
    }
}

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

using System;
using System.Threading.Tasks;

namespace Example10
{
    class Program
    {
        static void Main(string[] args)
        {
            ParallelLoopResult result = Parallel.
            For(0, 1000, (int i, ParallelLoopState loopState) =>
            {
                if (i == 500)
                {
                    Console.WriteLine("Breaking loop");
                    loopState.Break();
                }
                return;
            });
        }
    }
}

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.

That’s all for now! This is no way a complete guide for tasks in C# and should be considered a starting point. We will learn more about tasks handling and task cancellation in future chapters.

References


comments powered by Disqus