C# Programming/The .NET Framework/Threading
Threads are tasks that can run concurrently to other threads and can share data. When your program starts, it creates a thread for the entry point of your program, usually a Main
function. So, you can think of a "program" as being made up of threads. The .NET Framework allows you to use threading in your programs to run code in parallel to each other. This is often done for two reasons:
- If the thread running your graphical user interface performs time-consuming work, your program may appear to be unresponsive. Using threading, you can create a new thread to perform tasks and report its progress to the GUI thread.
- On computers with more than one CPU or CPUs with more than one core, threads can maximize the use of computational resources, speeding up tasks.
The Thread
class
[edit | edit source]The System.Threading.Thread
class exposes basic functionality for using threads. To create a thread, you simply create an instance of the Thread
class with a ThreadStart
or ParameterizedThreadStart
delegate pointing to the code the thread should start running. For example:
using System;
using System.Threading;
public static class Program
{
private static void SecondThreadFunction()
{
while (true)
{
Console.WriteLine("Second thread says hello.");
Thread.Sleep(1000); // pause execution of the current thread for 1 second (1000 ms)
}
}
public static void Main()
{
Thread newThread = new Thread(new ThreadStart(SecondThreadFunction));
newThread.Start();
while (true)
{
Console.WriteLine("First thread says hello.");
Thread.Sleep(500); // pause execution of the current thread for half a second (500 ms)
}
}
}
You should see the following output:
Second thread says hello. First thread says hello. First thread says hello. Second thread says hello. First thread says hello. First thread says hello. ...
Notice that the while keyword is needed because as soon as the function returns, the thread exits, or terminates.
ParameterizedThreadStart
[edit | edit source]The void ParameterizedThreadStart(object obj)
delegate allows you to pass a parameter to the new thread:
using System;
using System.Threading;
public static class Program
{
private static void SecondThreadFunction(object param)
{
while (true)
{
Console.WriteLine("Second thread says " + param.ToString() + ".");
Thread.Sleep(500); // pause execution of the current thread for half a second (500 ms)
}
}
public static void Main()
{
Thread newThread = new Thread(new ParameterizedThreadStart(SecondThreadFunction));
newThread.Start(1234); // here you pass a parameter to the new thread
while (true)
{
Console.WriteLine("First thread says hello.");
Thread.Sleep(1000); // pause execution of the current thread for a second (1000 ms)
}
}
}
The output is:
First thread says hello. Second thread says 1234. Second thread says 1234. First thread says hello. ...
Sharing Data
[edit | edit source]Although we could use ParameterizedThreadStart
to pass parameter(s) to threads, it is not typesafe and is clumsy to use. We could exploit anonymous delegates to share data between threads, however:
using System;
using System.Threading;
public static class Program
{
public static void Main()
{
int number = 1;
Thread newThread = new Thread(new ThreadStart(delegate
{
while (true)
{
number++;
Console.WriteLine("Second thread says " + number.ToString() + ".");
Thread.Sleep(1000);
}
}));
newThread.Start();
while (true)
{
number++;
Console.WriteLine("First thread says " + number.ToString() + ".");
Thread.Sleep(1000);
}
}
}
Notice how the body of the anonymous delegate can access the local variable number
.
Asynchronous Delegates
[edit | edit source]Using anonymous delegates can lead to a lot of syntax, confusion of scope, and lack of encapsulation. However with the use of lambda expressions, some of these problems can be mitigated. Instead of anonymous delegates, you can use asynchronous delegates to pass and return data, all of which is type safe. It should be noted that when you use an asynchronous delegate, you are actually queuing a new thread to the thread pool. Also, using asynchronous delegates forces you to use the asynchronous model.
using System;
public static class Program
{
delegate int del(int[] data);
public static int SumOfNumbers(int[] data)
{
int sum = 0;
foreach (int number in data) {
sum += number;
}
return sum;
}
public static void Main()
{
int[] numbers = new int[] { 1, 2, 3, 4, 5 };
del func = SumOfNumbers;
IAsyncResult result = func.BeginInvoke(numbers, null, null);
// I can do stuff here while numbers is being added
int sum = func.EndInvoke(result);
sum = 15
}
}
Synchronization
[edit | edit source]In the sharing data example, you may have noticed that often, if not all of the time, you will get the following output:
First thread says 2. Second thread says 3. Second thread says 5. First thread says 4. Second thread says 7. First thread says 7.
One would expect that at least, the numbers would be printed in ascending order! This problem arises because of the fact that the two pieces of code are running at the same time. For example, it printed 3, 5, then 4. Let us examine what may have occurred:
- After "First thread says 2", the first thread incremented
number
, making it 3, and printed it. - The second thread then incremented
number
, making it 4. - Just before the second thread got a chance to print
number
, the first thread incrementednumber
, making it 5, and printed it. - The second thread then printed what
number
was before the first thread incremented it, that is, 4. Note that this may have occurred due to console output buffering.
The solution to this problem is to synchronize the two threads, making sure their code doesn't interleave like it did. C# supports this through the lock keyword. We can put blocks of code under this keyword:
using System;
using System.Threading;
public static class Program
{
public static void Main()
{
int number = 1;
object numberLock = new object();
Thread newThread = new Thread(new ThreadStart(delegate
{
while (true)
{
lock (numberLock)
{
number++;
Console.WriteLine("Second thread says " + number.ToString() + ".");
}
Thread.Sleep(1000);
}
}));
newThread.Start();
while (true)
{
lock (numberLock)
{
number++;
Console.WriteLine("First thread says " + number.ToString() + ".");
}
Thread.Sleep(1000);
}
}
}
The variable numberLock
is needed because the lock keyword only operates on reference types, not value types. This time, you will get the correct output:
First thread says 2. Second thread says 3. Second thread says 4. First thread says 5. Second thread says 6. ...
The lock keyword operates by trying to gain an exclusive lock on the object passed to it (numberLock
). It will only release the lock when the code block has finished execution (that is, after the }
). If an object is already locked when another thread tries to gain a lock on the same object, the thread will block (suspend execution) until the lock is released, and then lock the object. This way, sections of code can be prevented from interleaving.
Thread.Join()
[edit | edit source]The Join
method of the Thread
class allows a thread to wait for another thread, optionally specifying a timeout:
using System;
using System.Threading;
public static class Program
{
public static void Main()
{
Thread newThread = new Thread(new ThreadStart(delegate
{
Console.WriteLine("Second thread reporting.");
Thread.Sleep(5000);
Console.WriteLine("Second thread done sleeping.");
}));
newThread.Start();
Console.WriteLine("Just started second thread.");
newThread.Join(1000);
Console.WriteLine("First thread waited for 1 second.");
newThread.Join();
Console.WriteLine("First thread finished waiting for second thread. Press any key.");
Console.ReadKey();
}
}
The output is:
Just started second thread. Second thread reporting. First thread waited for 1 second. Second thread done sleeping. First thread finished waiting for second thread. Press any key.