A thread is a program execution path to the CPU. Each time a statement is executed, it needs a pathway to the CPU that then translates the binary code to what we see on the screen. When you run several programs at once, the computer must use a scheduler to determine the next thread in sequence that should take the CPU's time.
Threads are component of processes. Each process can have multiple threads. When a process contains multiple threads, it's said to be multithreaded. Multithreaded coding allows multiple threads to execute concurrently. This means that several threads can run before another is finished executing.
Take any one of the previous chapter's sample programs and run it in the debugger. You'll notice that all execution is linear. You create a thread whenever you execute program statements. The Main thread is always used by default, but all code execution runs through this main thread unless you instruct the computer to do otherwise. The processor waits for the first line of code to execute before it moves to the next. One example is when you prompt the user for input. The program waits for a response and then processes the response. Only after the response is processed does the next statement execute.
For small projects, waiting for the next line of execution doesn't seem like a problem. However, imagine if you have a process that takes a minute or even 30 seconds. You're forced to wait for the process to finish before you can do anything else. If the process takes an hour, it would be impossible to finish other tasks until your computer finished execution. That's where multithreading is beneficial.
With multithreading, you tell the program to open a new thread in parallel to the main thread. When you have multiple threads available, you can run different statements in parallel to one another. The result is that you have multiple statements running at the same time, so neither has to wait for execution of the other.
Threading is also useful when you want to allow a process to run in the background. Let's assume you have a program that you want to run in the background. It collects hard drive information, so it must go through gigabytes of data before it finishes. This could take several minutes (even hours), so you need to run the statements while allowing the user to perform other tasks on the machine. You do this using multithreaded programming. You create a new thread and allow program execution for the user's tasks. Multithreading makes your program much more user friendly.
Threading Examples
The best way to learn threading is to look at some examples. Let's first look at a simple Hello World program.
using System.Threading;
public class HelloWorld
{
public static void Main()
{
System.Console.WriteLine("Hello, World!");
}
}
We've seen this program before in previous chapters. This program named HelloWorld does nothing more than print the value "Hello, World!" to the screen. We added a library to the code. To use threading in your programs, you need the System.Threading library. You also need the System library, but Visual Studio adds this library to the top of your pages when you create a new C# class file. We're adding the threading library to our example code to remind you to include it when you work with these samples.
Suppose you want to print this message 10 times to the user's screen. Any other processes would have to wait until the HelloWorld program finished. What if we wanted to print the numbers 1 to 10 while the program was printing Hello World to the screen? We would do this through multithreading.
Let's first create an added method that writes the values 1 to 10 to the screen.
using System.Threading;
public class HelloWorld
{
public static void Main()
{
this.WriteNumbers();
System.Console.WriteLine("Hello, World!");
}
static void WriteNumbers()
{
for (int i = 1; i <= 10; i++)
{
Console.Write ("The number {0}", i);
Console.WriteLine();
}
}
}
In the above code, we added a WriteNumbers method. This method has a for statement that iterates through the values 1 to 10 and prints the output to the screen. As you can see, nothing unique that we haven't seen already is included in the method. The only statement that hasn't been seen is the "this" notation. The "this" notation tells the compiler that you want to use a method within the current class. The "this" notation simply means "this class" or "this object."
The example code first calls the WriteNumbers method, and then it prints Hello World to the screen. The program must wait for the numbers to print on the screen before it moves on to the Hello World statement. Let's change this linear process to a multithreaded program.
First, let's create a new thread in the Main method.
using System.Threading;
public class HelloWorld
{
public static void Main()
{
Thread't = new Thread (WriteNumbers);
t.Start();
System.Console.WriteLine("Hello, World!");
}
static void WriteNumbers()
{
for (int i = 1; i <= 10; i++)
{
Console.Write ("The number {0}", i);
Console.WriteLine();
}
}
}
Notice that we replaced the this.WriteNumbers() statement with a new thread. The Thread class is used to create multithreaded statements in C#. In this example, we instantiate the class and assign its instance to the't variable. Notice that we pass the parallel method to the thread. We want to run the WriteNumbers method in parallel with the Hello World output, so we pass this method to the class.
At this point, nothing has happened except that we told the compiler that we want to create a multithreaded program. To start the thread, we use the t.Start() method. This tells the program to start execution of the WriteNumbers method.
If you run this program in Visual Studio, you'll see the results of the multithreading. The WriteNumbers method begins and at the same time it iterates through the numbers, "Hello World" is also displayed. At this point, you have no control of when the Hello World output is shown. It can show after the #2 is shown or after #10 is printed. The timing depends on the speed of your computer and any other processes handled by the processor. Remember that processing relies on the operating system scheduler.
The C# framework also lets you run the same function multiple times in different threads. Let's assume you want to print the numbers 1 to 10 in two instances, but you want those instances to run in parallel. Let's take a look at the code.
using System.Threading;
public class HelloWorld
{
public static void Main()
{
Thread't = new Thread (WriteNumbers);
t.Start();
this.WriteNumbers();
}
static void WriteNumbers()
{
for (int i = 1; i <= 10; i++)
{
Console.Write ("The number {0}", i);
Console.WriteLine();
}
}
}
In the above code, we swapped out the Hello World output for a new instance of the WriteNumbers method. In this code, we run the WriteNumbers method twice in parallel with each other. Again, you have no control over when the values print to the screen or in what order. All you know is that the methods run at the same time without waiting for the other to finish.
The C# language lets you share variables between threads. Let's take a look at an example.
using System.Threading;
public class HelloWorld
{
bool finished;
public void Main()
{
Thread't = new Thread (WriteNumbers);
t.Start();
this.WriteNumbers()
}
static void WriteNumbers()
{
if (!finished)
{
for (int i = 0; i < 10; i++)
{
Console.Write ("The number {0}", i);
Console.WriteLine();
}
finished = true;
}
}
}
We created a Boolean variable in the code above to indicate when we want to quit the program. Since both threads share the finished Boolean variable, we can identify the way this code will execute. This type of coding is called thread safe since the output is determinate.
If we change the variable to static, the program is no longer discriminate. Let's take a look at the code.
using System.Threading;
public class HelloWorld
{
static bool finished;
public void Main()
{
Thread't = new Thread (WriteNumbers);
t.Start();
t.WriteNumbers()
}
static void WriteNumbers()
{
if (!finished)
{
for (int i = 0; i < 10; i++)
{
Console.Write ("The number {0}", i);
Console.WriteLine();
}
finished = true;
}
}
}
When the same function shares the same thread, the variables used are shared. When finished is set to true for one instance, it's set to true for all the others.
A problem occurs when we set shared variables to static. When static variables are used, there is no lock on the variable during execution, so the output is indeterminate. When output is said to be indeterminate, the threads are seen as unsafe since we can't determine the output.
To avoid this phenomenon, you can set a locking condition on the thread. The lock statement stops execution of one thread until the main part of the method is completed. The result is that the static variable is evaluated twice in a linear way but only in certain sections of your code. This process makes your program thread safe. Let's see what the code looks like.
using System.Threading;
public class HelloWorld
{
static bool finished;
static readonly object locker = new object();
public void Main()
{
Thread't = new Thread (WriteNumbers);
t.Start();
t.WriteNumbers()
}
static void WriteNumbers()
{
lock (locker)
{
if (!finished)
{
for (int i = 0; i < 10; i++)
{
Console.Write ("The number {0}", i);
Console.WriteLine();
}
finished = true;
}
}
}
}
Notice that we added two lines of code. The first one is a readonly object. We haven't seen a readonly variable. This type of variable is static and cannot dynamically change like other standard variables. The locker variable is then used in the WriteNumbers method to lock the process until the previous thread is finished processing. In this example, we know that the for loop will only iterate once until the finished Boolean variable is changed to true.
We've mentioned that you can't control the order in which a thread will execute statements since both methods run simultaneously. However, you can force one thread to pause and wait until another thread is completed. The Thread class has a method named Join that lets you stop execution of one thread to wait for another.
For instance, suppose that you want the first WriteNumbers call to execute first, and then you want the second call to execute after the first one is completed. We can change our previous code to work with the Join method.
using System.Threading;
public class HelloWorld
{
static bool finished;
static readonly object locker = new object();
public void Main()
{
Thread't = new Thread (WriteNumbers);
t.Start();
t.Join();
t.WriteNumbers()
}
static void WriteNumbers()
{
lock (locker)
{
if (!finished)
{
for (int i = 0; i < 10; i++)
{
Console.Write ("The number {0}", i);
Console.WriteLine();
}
finished = true;
}
}
}
}
We added the "t.Join()" statement to our code. In the above code, the first thread that contains WriteNumbers executes when t.Start() runs. Then, the t.Join() statement executes and pauses execution of your code. This stops the second instance from running until the first executes. The Join method is a way to control the order in which threads execute. We only have two threads, but imagine having 10 or 20 threads. The Join statement controls the order in which each of them executes.
There is one more way of controlling execution and timing. The Thread class also has a Sleep method. This method works similarly except it forces the thread to pause for a specified amount of time. The time parameter is set in milliseconds. Keep this in mind when you set your timeout value. A value of 1000 milliseconds is equal to 1 second.
Let's take a look at example code that uses the Sleep method.
using System.Threading;
public class HelloWorld
{
static bool finished;
static readonly object locker = new object();
public void Main()
{
Thread't = new Thread (WriteNumbers);
t.Start();
t.Sleep(1000);
this.WriteNumbers()
}
static void WriteNumbers()
{
lock (locker)
{
if (!finished)
{
for (int i = 0; i < 10; i++)
{
Console.Write ("The number {0}", i);
Console.WriteLine();
}
finished = true;
}
}
}
}
We replaced the Join statement from the previous example with the Sleep method. Notice that the Sleep method has a parameter set to 1000, which means the thread pauses for 1000 milliseconds or 1 total second. When the Sleep method executes, the thread is paused but the second WriteNumbers thread continues. You can guess that the second method would then finish before the first one continued from Sleep to full execution.
You can prematurely return the thread to its execution by calling the Sleep method again and sending it a value of 0. The new .NET framework versions (version 4 and newer) have a method named Yield. The Yield method performs the same functionality as using the Sleep method with a 0 value for its parameter.
When both the Sleep or Join method are called, remember that execution times out or pauses. This means that no CPU resources are used. When either of these methods is called, CPU resources are freed until execution returns to the thread.
We mentioned that multithreading is a way to force a process to run in the background. You can set a background process using the "IsBackground" property in the Thread class. Suppose we want to run the first thread in the background. Let's take a look at the example code.
using System.Threading;
public class HelloWorld
{
static bool finished;
static readonly object locker = new object();
public void Main()
{
Thread't = new Thread (WriteNumbers);
t.Start();
t.IsBackground = true;
t.Sleep(1000);
this.WriteNumbers()
}
static void WriteNumbers()
{
lock (locker)
{
if (!finished)
{
for (int i = 0; i < 10; i++)
{
Console.Write ("The number {0}", i);
Console.WriteLine();
}
finished = true;
}
}
}
}
Only one statement was added from the previous code example. The "t.IsBackground" property is set to true. This lets you run the process in the background while the second call runs for the user to see. This property is useful when you want to allow a process to run in the background while the user is still able to work with your program without waiting for it to complete first.
Threading is a powerful tool that's a bit more complex to understand than other concepts. However, if you ever plan to create programs that have long processes that take too long to complete, you'll need a multithreaded structure. For instance, a reporting application might need an hour to gather data for a report, and multithreading processes let the reporting functionality run while the user works with other parts of your code.
With a little practice, you can create multithreaded programs for a better user interface and experience to improve performance.