Multi-Threading in C# With Tasks

Using the Task Parallel Library in .NET 4.0

Screenshot of Folders Application

The computer programming term "thread" is short for thread of execution, in which a processor follows a specified path through your code. The concept of following more than one thread at a time introduces the subject of multi-tasking and multi-threading.

An application has one or more processes in it. Think of a process as a program running on your computer. Now each process has one or more threads.

A game application might have a thread to load resources from disk, another to do AI, and another to run the game as a server.

In .NET/Windows, the operating system allocates processor time to a thread. Each thread keeps track of exception handlers and the priority at which it runs, and it has somewhere to save the thread context until it runs. Thread context is the information that the thread needs to resume.

Multi-Tasking With Threads

Threads take up a bit of memory and creating them takes a little time, so usually you don't want to use many. Remember, they compete for processor time. If your computer has multiple CPUs, then Windows or .NET might run each thread on a different CPU, but if several threads run on the same CPU, then only one can be active at a time and switching threads takes time.

The CPU runs a thread for a few million instructions, and then it switches to another thread. All of the CPU registers, current program execution point and stack have to be saved somewhere for the first thread and then restored from somewhere else for the next thread.

Creating a Thread

In the namespace System.Threading, you'll find the thread type. The constructor thread (ThreadStart) creates an instance of a thread. However, in recent C# code, it's more likely to pass in a lambda expression that calls the method with any parameters.

If you're unsure about lambda expressions, it might be worth checking out LINQ.

Here is an example of a thread that is created and started:

using System;

using System.Threading;

namespace ex1
{
    class Program
    {

        public static void Write1()
        {
            Console.Write('1') ;
            Thread.Sleep(500) ;
        }

        static void Main(string[] args)
        {
            var task = new Thread(Write1) ;
            task.Start() ;
            for (var i = 0; i < 10; i++)
            {
                Console.Write('0') ;
                Console.Write (task.IsAlive ? 'A' : 'D') ;
                Thread.Sleep(150) ;
            }
            Console.ReadKey() ;
        }
    }
}

All this example does is write "1" to the console. The main thread writes a "0" to the console 10 times, each time followed by an "A" or "D" depending on whether the other thread is still Alive or Dead.

The other thread only runs once and writes a "1." After the half-second delay in the Write1() thread, the thread finishes and the Task.IsAlive in the main loop now returns "D."

Thread Pool and Task Parallel Library

Instead of creating your own thread, unless you really need to do it, make use of a Thread Pool. From .NET 4.0, we have access to the Task Parallel Library (TPL). As in the previous example, again we need a bit of LINQ, and yes, it's all lambda expressions.

Tasks uses the Thread Pool behind the scenes but make better use of the threads depending on the number in use.

The main object in the TPL is a Task. This is a class that represents an asynchronous operation. The commonest way to start things running is with the Task.Factory.StartNew as in:

Task.Factory.StartNew(() => DoSomething());

Where DoSomething() is the method that is run. It's possible to create a task and not have it run immediately. In that case, just use Task like this:

var t = new Task(() => Console.WriteLine("Hello"));
...
t.Start();

That doesn't start the thread until the .Start() is called. In the example below, are five tasks.

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

namespace ex1
{
    class Program
    {

        public static void Write1(int i)
        {
            Console.Write(i) ;
            Thread.Sleep(50) ;
        }

        static void Main(string[] args)
        {
            
            for (var i = 0; i < 5; i++)
            {
                var value = i;
                var runningTask = Task.Factory.StartNew(()=>Write1(value)) ;
            }
            Console.ReadKey() ;
        }
    }
}

Run that and you get the digits 0 through 4 output in some random order such as 03214. That's because the order of task execution is determined by .NET.

You might be wondering why the var value = i is needed. Try removing it and calling Write(i), and you'll see something unexpected like 55555. Why is this? It's because the task shows the value of i at the time that the task is executed, not when the task was created. By creating a new variable each time in the loop, each of the five values is correctly stored and picked up.