C# Threads Basics
Every program you have written so far ran on one thread: the main thread. It executes your statements one after another, so if one method has to wait five seconds for a database, everything after it waits too. In this section we look at what a thread is, why one thread is often not enough, and how the Thread class in System.Threading lets a program run several methods side by side. We create threads, pass data into them, get results back, watch every state a thread goes through, and see what happens to a thread that is still running when the program ends.
Every program below was built and run with .NET 8, and every output shown is the real output of that run.
Objectives
By the end of the section you should be able to:
- Explain the relationship between an application, its process and its threads, and name the main thread.
- Explain why a single-threaded program runs sequentially, and why a long wait blocks everything after it.
- Create and start threads with the
Threadclass, using a method name, aThreadStartdelegate, an anonymous method or a lambda. - Pass data into a thread with
ParameterizedThreadStart, then in a type-safe way with a helper class, and return a result with a callback delegate. - Follow a thread through its states (
Unstarted,Running,WaitSleepJoin,Stopped) usingThreadState,IsAlive,SleepandJoin. - Predict what happens to foreground and background threads when the main thread ends.
Part 1: processes and threads
Multitasking and processes
Multitasking means performing several tasks at the same time. Windows is a multitasking operating system: you can have a browser, Visual Studio, Notepad and a media player open together.
To run each application, the operating system uses a process. One application runs in one process, and every process gets its own process ID (PID). You can see them in Task Manager: the Processes tab lists your applications and the background processes (services) the operating system needs, and the Details tab shows the PID of each one.
Threads and the main thread
A process does not execute your code by itself. Inside the process, a thread does the work:
- A thread is a unit of a process that executes the application's code. It is sometimes called a lightweight process.
- By default, every process gets one thread. That thread is the main thread, and it starts executing your program at
Main. - So by default every application is single-threaded. Running several threads in one process is something the programmer does on purpose.
- A thread belongs to its process: a thread of one application cannot run the code of another application.
Operating system
+-- Process (PID 540) -> application 1
| +-- main thread
+-- Process (PID 812) -> application 2
+-- main thread
+-- worker thread
+-- worker threadAll the threading classes of .NET live in the System.Threading namespace, so every program in this section starts with using System.Threading;.
Worked example 1: naming the main thread
The static property Thread.CurrentThread returns the thread that is executing the current line. The instance property Name gets or sets that thread's name.
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread t = Thread.CurrentThread;
Console.WriteLine("Name before: [" + t.Name + "]");
Console.WriteLine("Name is null: " + (t.Name == null));
t.Name = "Main Thread";
Console.WriteLine("t.Name: " + t.Name);
Console.WriteLine("Thread.CurrentThread.Name: " + Thread.CurrentThread.Name);
}
}Predict what the first two lines print before you read the output.
Name before: []
Name is null: True
t.Name: Main Thread
Thread.CurrentThread.Name: Main Thread- A new thread has no name:
Nameisnull, which prints as nothing between the brackets. CurrentThreadis static, so we call it on the class. It returns aThreadobject, andNameis an instance property of that object.tandThread.CurrentThreadare the same thread (only one thread runs this program), so both lines print the name we set.
Names are not needed in real programs. We use them in this section so that the output tells us which thread printed each line.
Part 2: one thread versus many
Worked example 2: a single-threaded program waits
Here the main thread calls three methods. Method2 pretends to call a slow database: Thread.Sleep(5000) pauses the thread that calls it for 5000 milliseconds. A Stopwatch (from System.Diagnostics) measures how long Main takes.
using System;
using System.Diagnostics;
using System.Threading;
class Program
{
static void Main()
{
Stopwatch sw = Stopwatch.StartNew();
Console.WriteLine("Main started");
Method1();
Method2();
Method3();
Console.WriteLine("Main ended after " + sw.Elapsed.TotalSeconds.ToString("0.0") + " s");
}
static void Method1()
{
for (int i = 1; i <= 3; i++)
Console.WriteLine("Method1: " + i);
}
static void Method2()
{
for (int i = 1; i <= 3; i++)
{
if (i == 2)
{
Console.WriteLine("Method2: database operation started");
Thread.Sleep(5000); // pretend this is a slow DB call
Console.WriteLine("Method2: database operation completed");
}
Console.WriteLine("Method2: " + i);
}
}
static void Method3()
{
for (int i = 1; i <= 3; i++)
Console.WriteLine("Method3: " + i);
}
}Predict: when does Method3 print its first line, and how long does Main take?
Main started
Method1: 1
Method1: 2
Method1: 3
Method2: 1
Method2: database operation started
Method2: database operation completed
Method2: 2
Method2: 3
Method3: 1
Method3: 2
Method3: 3
Main ended after 5.0 sOne thread means sequential execution: Method2 cannot start before Method1 finishes, and Method3 cannot start before Method2 finishes. The whole program waited the full 5 seconds, although Method3 has nothing to do with the database.
Why that is a problem
- While the thread sleeps, the CPU has nothing to do for this program: its time is wasted.
- Real programs wait for databases, web APIs and files, often for many seconds.
- In a program with a user interface, the same single thread also draws the window and handles clicks, so a long operation freezes the whole window until it finishes.
The fix is multithreading: let the long operation run on another thread, so the rest of the program does not wait for it.
Worked example 3: three threads
We keep the same three methods (each now also prints which thread runs it) and give each one its own thread. new Thread(Method1) creates a thread that will run Method1, and Start() starts it. Until you call Start, the method never runs.
using System;
using System.Diagnostics;
using System.Threading;
class Program
{
static void Main()
{
Stopwatch sw = Stopwatch.StartNew();
Thread.CurrentThread.Name = "Main";
Console.WriteLine("Main started");
Thread t1 = new Thread(Method1) { Name = "Thread1" };
Thread t2 = new Thread(Method2) { Name = "Thread2" };
Thread t3 = new Thread(Method3) { Name = "Thread3" };
t1.Start();
t2.Start();
t3.Start();
Console.WriteLine("Main ended after " + sw.Elapsed.TotalSeconds.ToString("0.0") + " s");
}
static void Method1()
{
Console.WriteLine("Method1 started using " + Thread.CurrentThread.Name);
for (int i = 1; i <= 3; i++)
Console.WriteLine("Method1: " + i);
Console.WriteLine("Method1 ended using " + Thread.CurrentThread.Name);
}
static void Method2()
{
Console.WriteLine("Method2 started using " + Thread.CurrentThread.Name);
for (int i = 1; i <= 3; i++)
{
if (i == 2)
{
Console.WriteLine("Method2: database operation started");
Thread.Sleep(5000);
Console.WriteLine("Method2: database operation completed");
}
Console.WriteLine("Method2: " + i);
}
Console.WriteLine("Method2 ended using " + Thread.CurrentThread.Name);
}
static void Method3()
{
Console.WriteLine("Method3 started using " + Thread.CurrentThread.Name);
for (int i = 1; i <= 3; i++)
Console.WriteLine("Method3: " + i);
Console.WriteLine("Method3 ended using " + Thread.CurrentThread.Name);
}
}Predict: will the output come out in the same order every time?
We ran this program three times. Here is the second run:
Main started
Method1 started using Thread1
Method2 started using Thread2
Method3 started using Thread3
Method1: 1
Method2: 1
Method2: database operation started
Method1: 2
Method1: 3
Method1 ended using Thread1
Method3: 1
Method3: 2
Method3: 3
Method3 ended using Thread3
Main ended after 0.0 s
Method2: database operation completed
Method2: 2
Method2: 3
Method2 ended using Thread2And this is the order in which the loops printed their numbers in each of the three runs:
| Run | Order of the loop lines | Main ended after |
|---|---|---|
| 1 | Method1 1-3, Method2 1, Method3 1-3, Method2 2-3 | 0.1 s |
| 2 | Method1 1, Method2 1, Method1 2-3, Method3 1-3, Method2 2-3 | 0.0 s |
| 3 | Method3 1-3, Method1 1-3, Method2 1, Method2 2-3 | 0.1 s |
- The order changes from run to run. You cannot predict it, and that is normal for a multithreaded program.
Method3no longer waits forMethod2's database call.Mainfinished almost immediately: it only created and started the threads. The program itself still ran for about 5 seconds (we measured 5.05 s with thetimecommand) becauseThread2was still sleeping. Part 4 explains why the program waited for it.
Taking turns on the CPU
When several threads want to run, the operating system gives each one a small time slice (some milliseconds). If a thread does not finish within its slice, the next thread gets the CPU, then the next, and so on, round and round until every thread has finished:
CPU: | t1 | t2 | t3 | t1 | t2 | t3 | t1 | ...The lecture video describes one CPU core: at any single instant only one thread runs on it, and the switching is so fast that the threads look simultaneous. Our lab machines have several cores, so two threads can also run at exactly the same moment on different cores. Either way, we do not control which thread runs when, or for how long; that is why the output order is unpredictable.
Main thread and worker threads
- The main thread is created by the process and starts at
Main. Even in a multithreaded program, execution always starts on the main thread. - The threads that the main thread creates (
t1,t2,t3above) are called worker threads or child threads of the main thread. - If
Method1created another thread, that new thread would be a child oft1.
When multithreading helps
Use threads for methods that are independent of each other. If one method needs the result of another, they cannot simply run side by side: the second must wait for the first (Part 4 shows how with Join). And if everything must run strictly one after another anyway, extra threads only add cost, because creating a thread also takes time.
Part 3: the Thread class
What the class gives you
The Thread class (namespace System.Threading) creates and controls threads. It can create foreground and background threads, set a thread's priority, and report its current state. These are the members we use in this section:
| Member | Kind | What it does |
|---|---|---|
Thread.CurrentThread | static property | the thread executing the current line |
Name | property | get or set the thread's name (null by default) |
ThreadState | property | the current state of the thread |
IsAlive | property | True after Start until the thread ends |
IsBackground | property | get or set whether it is a background thread |
| Method | What it does |
|---|---|
Start() | starts a thread whose method takes no parameter |
Start(value) | starts a thread and passes value to its method |
Thread.Sleep(ms) | static: pauses the calling thread for ms milliseconds |
Join() | the calling thread waits until this thread has finished |
The class also has Priority, ManagedThreadId, Interrupt, Abort, Suspend and Resume; they belong to later topics.
Worked example 4: the Thread class is sealed
Can you write your own class that inherits from Thread?
using System;
using System.Threading;
class MyThread : Thread
{
}
class Program
{
static void Main()
{
}
}The compiler refuses:
Program.cs(4,18): error CS0509: 'MyThread': cannot derive from sealed type 'Thread'Thread is a sealed class, so no class can inherit from it. You use it as it is: create an instance and give it the method to run.
The constructor takes a delegate
A thread needs an entry point: the method where it starts executing. The constructor of Thread receives that entry point as a delegate, which is a type-safe pointer to a method. There are two delegate types it accepts:
| Delegate | Method signature it matches | Start the thread with |
|---|---|---|
ThreadStart | void Method() | t.Start() |
ParameterizedThreadStart | void Method(object x) | t.Start(value) |
- Both return
void: a thread method cannot return a value directly. ParameterizedThreadStarttakes exactly one parameter, and its type isobject.- Any other signature does not compile. You cannot use your own delegate type either: the constructor accepts only these two.
- When you call
Start, the thread's state changes fromUnstarted, and the thread invokes the delegate, which runs your method.
Worked example 5: five ways to create the same thread
All five threads below run the same method. The first form is the shortest; the compiler turns it into the second form for you.
using System;
using System.Threading;
class Program
{
static void Main()
{
// 1. pass the method name: the compiler builds the ThreadStart for us
Thread t1 = new Thread(DisplayNumbers) { Name = "t1" };
// 2. build the ThreadStart ourselves, then pass the variable
ThreadStart ts = new ThreadStart(DisplayNumbers);
Thread t2 = new Thread(ts) { Name = "t2" };
// 3. build it inline
Thread t3 = new Thread(new ThreadStart(DisplayNumbers)) { Name = "t3" };
// 4. an anonymous method
Thread t4 = new Thread(delegate () { DisplayNumbers(); }) { Name = "t4" };
// 5. a lambda expression
Thread t5 = new Thread(() => DisplayNumbers()) { Name = "t5" };
t1.Start();
t2.Start();
t3.Start();
t4.Start();
t5.Start();
}
static void DisplayNumbers()
{
Console.WriteLine(Thread.CurrentThread.Name + ": 1 2 3");
}
}Three runs of this program:
Run 1 Run 2 Run 3
t1: 1 2 3 t1: 1 2 3 t2: 1 2 3
t4: 1 2 3 t2: 1 2 3 t4: 1 2 3
t2: 1 2 3 t3: 1 2 3 t3: 1 2 3
t5: 1 2 3 t5: 1 2 3 t1: 1 2 3
t3: 1 2 3 t4: 1 2 3 t5: 1 2 3All five forms work the same way; only the order of the lines changes, because the five threads compete for the CPU.
Worked example 6: a method that does not fit
This method adds the numbers from 1 to 5 and returns the sum. Will new Thread(Sum) compile?
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread t1 = new Thread(Sum);
t1.Start();
}
static int Sum()
{
int result = 0;
for (int i = 1; i <= 5; i++)
result += i;
return result;
}
}No:
Program.cs(8,32): error CS1503: Argument 1: cannot convert from 'method group' to 'System.Threading.ParameterizedThreadStart'Sum returns int, but both delegates return void, so neither constructor accepts it. (The compiler names the last overload it tried.) We solve "how do I get a result back" with a callback later in this part.
Interactive: does the method fit?
Pick a return type and a parameter type for a method M and watch it being checked against ThreadStart and ParameterizedThreadStart. Each result is what the C# compiler really reported for new Thread(M) with that signature.
Worked example 7: passing data with ParameterizedThreadStart
To send data into the thread, the method takes one object parameter, and the value goes into Start. Here DisplayNumbers is an instance method, so we need an object of Program to reach it.
using System;
using System.Threading;
class Program
{
static void Main()
{
Program obj = new Program();
Thread t1 = new Thread(obj.DisplayNumbers);
t1.Start(5);
}
public void DisplayNumbers(object max)
{
int number = Convert.ToInt32(max); // unboxing and conversion
for (int i = 1; i <= number; i++)
Console.WriteLine("Number: " + i);
}
}Number: 1
Number: 2
Number: 3
Number: 4
Number: 5Start(5) passes 5 to the delegate, the delegate passes it to max, and Convert.ToInt32 turns the object back into an int.
Worked example 8: passing the wrong type
Change only one line: t1.Start("hi");. Predict: does the compiler stop you?
It compiles without any error, because "hi" is an object like everything else. The program then crashes when it runs:
Unhandled exception. System.FormatException: The input string 'hi' was not in a correct format.
at System.Number.ThrowFormatException[TChar](ReadOnlySpan`1 value)
at System.String.System.IConvertible.ToInt32(IFormatProvider provider)
at Program.DisplayNumbers(Object max) in Program.cs:line 15This is what not type-safe means:
- The parameter is
object, so any value is accepted, and a wrong value is caught only at run time, not at compile time. - Every value is boxed into an
objecton the way in and unboxed or converted on the way out, which costs time for nothing.
Worked example 9: passing data in a type-safe way
The fix: move the thread method into a helper class. The data goes into the class's constructor with its real type and is kept in a private field. The thread method then takes no parameter at all, so it fits ThreadStart.
using System;
using System.Threading;
class NumberHelper
{
private int _number;
public NumberHelper(int number)
{
_number = number;
}
public void DisplayNumbers()
{
for (int i = 1; i <= _number; i++)
Console.WriteLine("Number: " + i);
}
}
class Program
{
static void Main()
{
NumberHelper obj = new NumberHelper(5);
Thread t1 = new Thread(obj.DisplayNumbers);
t1.Start();
}
}The output is the same five lines as in worked example 7. Now try new NumberHelper("hi"):
Program.cs(24,45): error CS1503: Argument 1: cannot convert from 'string' to 'int'The mistake is caught at compile time, there is no boxing, and the constructor can take as many parameters, of any types, as the thread needs.
Worked example 10: returning data with a callback
A thread method returns void, so how does the result come back? The caller hands the helper a callback: a delegate that the thread invokes with the result when it finishes. The helper does not need to know which method that is; the caller decides.
- Declare a delegate type that matches the method that will receive the result.
- Give the helper class a private field of that delegate type and fill it from the constructor.
- At the end of the thread method, invoke the delegate with the result (if it is not
null). - The caller creates the delegate from its own method and passes it to the constructor.
using System;
using System.Threading;
public delegate void ResultCallback(int result);
class NumberHelper
{
private int _number;
private ResultCallback _callback;
public NumberHelper(int number, ResultCallback callback)
{
_number = number;
_callback = callback;
}
public void CalculateSum()
{
int result = 0;
for (int i = 1; i <= _number; i++)
{
Console.WriteLine(i);
result = result + i;
}
if (_callback != null)
_callback(result);
}
}
class Program
{
static void Main()
{
ResultCallback callback = new ResultCallback(ShowResult);
NumberHelper obj = new NumberHelper(10, callback);
Thread t1 = new Thread(obj.CalculateSum);
t1.Start();
}
static void ShowResult(int result)
{
Console.WriteLine("Sum of the numbers is: " + result);
}
}Predict the last line before reading the output.
1
2
3
4
5
6
7
8
9
10
Sum of the numbers is: 55The sum is 1 + 2 + ... + 10 = 55. Another class could create its own NumberHelper with a different callback method, and the result would go there instead, without changing NumberHelper at all.
Part 4: the life of a thread
Thread states
From the moment a Thread object is created until it ends, it moves through a life cycle. The lab document describes four states; the ThreadState property reports them with these names:
| Life-cycle state | ThreadState value | When |
|---|---|---|
| Unstarted | Unstarted | the object exists, Start not called yet |
| Ready / running | Running | started and not blocked (running, or waiting for its CPU turn) |
| Not runnable | WaitSleepJoin | blocked in Sleep, in Join, or waiting |
| Dead | Stopped | its method has finished |
Unstarted --Start()--> Running --Sleep / Join--> WaitSleepJoin
^ |
+------ time is up / --------+
joined thread ended
Running --method returns--> StoppedIsAlive is True from Start until the thread is Stopped.
Worked example 11: watching ThreadState
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread t = new Thread(Work);
Console.WriteLine("1) after new: " + t.ThreadState + ", IsAlive = " + t.IsAlive);
t.Start();
Thread.Sleep(500); // give the worker time to reach its Sleep
Console.WriteLine("3) while it sleeps: " + t.ThreadState + ", IsAlive = " + t.IsAlive);
t.Join();
Console.WriteLine("5) after Join: " + t.ThreadState + ", IsAlive = " + t.IsAlive);
}
static void Work()
{
Console.WriteLine("2) inside Work: " + Thread.CurrentThread.ThreadState);
Thread.Sleep(2000);
Console.WriteLine("4) Work is about to end");
}
}Predict the five lines, one at a time.
1) after new: Unstarted, IsAlive = False
2) inside Work: Running
3) while it sleeps: WaitSleepJoin, IsAlive = True
4) Work is about to end
5) after Join: Stopped, IsAlive = False- Line 1: the object exists but was never started.
- Line 2: a thread asking about itself is always
Running, because it is executing that line. - Line 3:
Mainslept 0.5 s, so the worker is inside its 2-secondSleep. - Line 5:
Joinreturned only afterWorkended, so the thread isStopped.
Worked example 12: Sleep
Thread.Sleep suspends the calling thread for a number of milliseconds (or a TimeSpan). A sleeping thread uses no CPU time. This example from the lab document prints six lines with a 4-second sleep after each one:
using System;
using System.Diagnostics;
using System.Threading;
class Program
{
static void Main()
{
Stopwatch stWatch = new Stopwatch();
stWatch.Start();
Thread oThread = new Thread(ProcessSleep);
oThread.Start();
oThread.Join();
stWatch.Stop();
TimeSpan ts = stWatch.Elapsed;
string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}", ts.Hours, ts.Minutes, ts.Seconds);
Console.WriteLine("TotalTime " + elapsedTime);
Console.WriteLine("work completed..!");
}
static void ProcessSleep()
{
for (int i = 0; i <= 5; i++)
{
Console.WriteLine("work is in progress..!");
Thread.Sleep(4000); // sleep for 4 seconds
}
}
}Predict the total time. The loop runs for i = 0 to 5, that is 6 times, and each pass sleeps 4 s: 6 x 4 = 24 s.
work is in progress..!
work is in progress..!
work is in progress..!
work is in progress..!
work is in progress..!
work is in progress..!
TotalTime 00:00:24
work completed..!Worked example 13: Join
t.Join() makes the calling thread wait until t has finished. Compare the same program with and without the Join line:
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread oThread = new Thread(MethodJoin);
oThread.Start();
oThread.Join();
Console.WriteLine("work completed..!");
}
static void MethodJoin()
{
for (int i = 1; i <= 3; i++)
{
Console.WriteLine("work is in progress..!");
}
}
}With Join:
work is in progress..!
work is in progress..!
work is in progress..!
work completed..!With the Join line commented out, we ran the program six times and got this output all six times:
work completed..!
work is in progress..!
work is in progress..!
work is in progress..!Without Join, Main does not wait: starting a thread takes a moment, so Main usually prints first. Join is how you get the sequential order back when you need it, for example when Main needs the worker's result.
Worked example 14: a foreground thread
Threads are either foreground or background. A new thread is a foreground thread (its IsBackground is False). A foreground thread keeps running until its work is finished, even if the main thread has already ended, and the program does not exit until every foreground thread has ended.
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread oThread = new Thread(WorkThread);
oThread.Start();
Console.WriteLine("Main Thread Quits..!");
}
static void WorkThread()
{
for (int i = 0; i <= 4; i++)
{
Console.WriteLine("Worker Thread is in progress..!");
Thread.Sleep(2000); // sleep for 2 seconds
}
Console.WriteLine("Worker Thread Quits..!");
}
}Predict the output and how long the program runs.
Worker Thread is in progress..!
Main Thread Quits..!
Worker Thread is in progress..!
Worker Thread is in progress..!
Worker Thread is in progress..!
Worker Thread is in progress..!
Worker Thread Quits..!The main thread quit at once, yet the worker finished all 5 passes and the program ran for 10.08 s (5 x 2 s of sleeping). This is also why the program of worked example 3 lasted 5 seconds after Main ended.
Worked example 15: a background thread
Add one line before Start: oThread.IsBackground = true;. A background thread quits when the foreground threads have ended: its lifetime depends on the main thread.
Thread oThread = new Thread(WorkThread);
oThread.IsBackground = true;
oThread.Start();
Console.WriteLine("Main Thread Quits..!");We ran it 20 times. Every run printed exactly two lines, one of them the worker's, and in our timed run the program ended after 0.05 s:
Main Thread Quits..!
Worker Thread is in progress..!(In 14 of the 20 runs the lines came in this order; in the other 6 the worker line came first.) The worker printed its first line, went to sleep for 2 seconds, and while it slept Main ended. No foreground thread was left, so the program exited and the background thread was stopped with it. "Worker Thread Quits..!" never printed.
A background thread also shows the flag in its state. Printing ThreadState of a background thread at the same moments as in worked example 11 gave Background, Unstarted, then Background while running, Background, WaitSleepJoin while sleeping, and Stopped after Join.
| Foreground (default) | Background | |
|---|---|---|
IsBackground | False | True |
| Main thread ends first | keeps running to the end | stopped when the last foreground thread ends |
| Keeps the program alive | yes | no |
| Typical use | work that must finish | helper work that may be dropped at exit |
Interactive: the thread timeline
The widget runs a small program with a main thread and two workers on one CPU core. Step through it tick by tick and watch each thread's ThreadState, IsAlive, the core, and the console. Turn Join on and off, make a worker a background thread, and change the sleep time to see when the process exits.
Part 5: a first look at Task
.NET also has a higher-level class, Task (namespace System.Threading.Tasks), that runs an action on a separate thread for you. Its constructor takes an action, here a lambda, and Start() schedules it. Like a thread, it does not run until you start it, and the main thread does not wait for it.
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
Console.WriteLine("Main thread started");
Task task = new Task(() =>
{
Console.WriteLine("Running task in separate thread");
int result = AddNumbers(5, 10);
Console.WriteLine("Result of addition: " + result);
});
task.Start();
Console.WriteLine("Main thread is done");
Console.ReadLine();
}
static int AddNumbers(int a, int b)
{
return a + b;
}
}Predict: which line comes right after Main thread started?
We ran it five times, pressing Enter after one second. In 3 of the 5 runs Main thread is done came before the task's first line; in the other 2 the task printed first. One of the runs:
Main thread started
Main thread is done
Running task in separate thread
Result of addition: 15The result was 15 in all five runs; only the order of the lines changed. Task belongs to later topics; for now, notice that it follows the same rule as Thread: once started, it runs independently of the main thread.
Seeing the threads in Visual Studio
You can watch the threads of a running program in the debugger:
- Put a breakpoint on a line inside the thread's method and start debugging (F5).
- When it stops, open Debug, Windows, Threads. The list shows the main thread and the worker threads, with their names. The Debug windows menu shows these entries only while you are debugging.
- Open Debug, Windows, Parallel Stacks to see each thread and the method it is executing.
- Press F10 to step. The yellow arrow shows which thread hit the breakpoint.
Task Manager's Details tab then shows your program's process and its PID.
Practice
About 30 minutes. Solve on paper first, then type the program and run it to check yourself. The thread timeline widget is useful for problems 1 and 4.
Practice 1: states (about 5 minutes)
Predict every line this program prints, in order.
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread t = new Thread(Work) { Name = "Worker" };
Console.WriteLine("A: " + t.ThreadState);
Console.WriteLine("B: " + t.IsBackground);
t.Start();
Thread.Sleep(1000);
Console.WriteLine("C: " + t.ThreadState + " " + t.IsAlive);
t.Join();
Console.WriteLine("D: " + t.ThreadState + " " + t.IsAlive);
}
static void Work()
{
Thread me = Thread.CurrentThread;
Console.WriteLine("W: " + me.Name + " " + me.ThreadState);
Thread.Sleep(3000);
}
}Practice 2: how long does it take? (about 6 minutes)
Three independent jobs take 2 s, 3 s and 1 s (simulated with Sleep). Predict both printed times, then explain the difference.
using System;
using System.Diagnostics;
using System.Threading;
class Program
{
static void Main()
{
Stopwatch sw = Stopwatch.StartNew();
Download();
Resize();
Save();
Console.WriteLine("One thread: " + sw.Elapsed.TotalSeconds.ToString("0.0") + " s");
sw.Restart();
Thread t1 = new Thread(Download);
Thread t2 = new Thread(Resize);
Thread t3 = new Thread(Save);
t1.Start(); t2.Start(); t3.Start();
t1.Join(); t2.Join(); t3.Join();
Console.WriteLine("Three threads: " + sw.Elapsed.TotalSeconds.ToString("0.0") + " s");
}
static void Download() { Thread.Sleep(2000); }
static void Resize() { Thread.Sleep(3000); }
static void Save() { Thread.Sleep(1000); }
}What would the second time be without the three Join calls?
Practice 3: a factorial on a thread (about 8 minutes)
Write a class FactorialHelper that computes n! (n factorial: 1 x 2 x ... x n) on a separate thread, in a type-safe way, and returns the result with a callback. The callback should print lines like 5! = 120. In Main, compute 5! and then 10!, each on its own thread, and make sure 5! is printed first. Then answer: what happens if a classmate writes new FactorialHelper("5", ShowResult)?
Practice 4: foreground or background? (about 5 minutes)
Predict the complete output twice: as written (the worker is a foreground thread), and after adding worker.IsBackground = true; before worker.Start();.
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread worker = new Thread(Tick);
worker.Start();
Thread.Sleep(1500);
Console.WriteLine("Main done");
}
static void Tick()
{
for (int i = 1; i <= 3; i++)
{
Console.WriteLine("tick " + i);
Thread.Sleep(1000);
}
Console.WriteLine("Worker done");
}
}Practice 5: ParameterizedThreadStart (about 6 minutes)
- Predict the output of this program.
- What happens if
t.Start(4)becomest.Start("four")? Does the compiler catch it? - Which of these methods can be passed to
new Thread(...), and with which delegate?void A(),void B(object x),void C(int x),int D().
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread t = new Thread(PrintSquares);
t.Start(4);
t.Join();
Console.WriteLine("Main done");
}
static void PrintSquares(object max)
{
int n = Convert.ToInt32(max);
for (int i = 1; i <= n; i++)
Console.WriteLine(i + " x " + i + " = " + (i * i));
}
}Answers
Practice 1
A: Unstarted
B: False
W: Worker Running
C: WaitSleepJoin True
D: Stopped False- A: created, not started. B: a new thread is a foreground thread.
- W: the worker asks about itself while executing, so it is
Running. - C: after 1 s the worker is still inside its 3-second
Sleep. - D:
Joinreturned, so the worker has finished.
Practice 2
One thread: 6.0 s
Three threads: 3.0 sWith one thread the waits add up: 2 + 3 + 1 = 6 s. With three threads the waits overlap, so the time is that of the longest job, 3 s, because Join makes Main wait for all three. Without the Join calls Main does not wait: we ran that version and it printed Three threads: 0.0 s while the three threads were still sleeping.
Practice 3
using System;
using System.Threading;
public delegate void FactorialCallback(int n, long result);
class FactorialHelper
{
private int _n;
private FactorialCallback _callback;
public FactorialHelper(int n, FactorialCallback callback)
{
_n = n;
_callback = callback;
}
public void Calculate()
{
long result = 1;
for (int i = 2; i <= _n; i++)
result = result * i;
if (_callback != null)
_callback(_n, result);
}
}
class Program
{
static void Main()
{
FactorialHelper helper = new FactorialHelper(5, ShowResult);
Thread t = new Thread(helper.Calculate);
t.Start();
t.Join();
helper = new FactorialHelper(10, ShowResult);
t = new Thread(helper.Calculate);
t.Start();
t.Join();
}
static void ShowResult(int n, long result)
{
Console.WriteLine(n + "! = " + result);
}
}5! = 120
10! = 3628800ngoes through the constructor as anint, so there is noobjectand no boxing.Calculatetakes no parameter, so it fitsThreadStart.- The first
JoinmakesMainwait for5!before it starts the second thread, so5!is always printed first. - The result is a
longbecause factorials grow fast. new FactorialHelper("5", ShowResult)does not compile:
Program.cs(31,54): error CS1503: Argument 1: cannot convert from 'string' to 'int'Practice 4
As written (foreground worker), the same output in 10 runs out of 10:
tick 1
tick 2
Main done
tick 3
Worker doneWith worker.IsBackground = true;, the same output in 10 runs out of 10:
tick 1
tick 2
Main doneThe worker prints at about 0 s, 1 s and 2 s; Main prints at 1.5 s. A foreground worker continues after Main ends. A background worker is stopped as soon as Main, the last foreground thread, ends, so tick 3 and Worker done never appear.
Practice 5
- Output:
1 x 1 = 1
2 x 2 = 4
3 x 3 = 9
4 x 4 = 16
Main donet.Start("four")compiles, because the parameter is anobject. The program crashes at run time:
Unhandled exception. System.FormatException: The input string 'four' was not in a correct format.- We compiled
new Thread(M)for each signature:
| Method | Compiles? | Delegate used |
|---|---|---|
void A() | yes | ThreadStart, start with Start() |
void B(object x) | yes | ParameterizedThreadStart, start with Start(value) |
void C(int x) | no, error CS1503 | none: the parameter must be object |
int D() | no, error CS1503 | none: the return type must be void |