Skip to content
wannacode

Threads

In short: A thread is a separate line of execution in a program; several threads run at the same time and share memory, so a shared counter is protected with lock or Interlocked.

Problem

Three reports, one at a time

The program downloads three monthly reports from the server. Here the download is a pause: Thread.Sleep(ms) stops the program for that many milliseconds. Thread is in System.Threading, which is included by default.
Program.cs
static void Download(string file, int ms)
{
Thread.Sleep(ms);
Console.WriteLine($"Downloaded {file}");
}
static void Main()
{
Download("march.pdf", 600);
Download("april.pdf", 200);
Download("may.pdf", 400);
Console.WriteLine("Done");
}
Output
Downloaded march.pdf
Downloaded april.pdf
Downloaded may.pdf
Done
The pauses are 0.6, 0.2 and 0.4 seconds, 1.2 seconds in total. The whole time, the program only waits for the network. If there were a "Cancel" button, it would not respond. And the three reports do not depend on each other, so they could easily be downloaded at the same time.

The idea

Process and thread

When you start the program, the operating system gives it a process: its own memory, its own open files. Inside the process, one or more threads run. A thread is a sequence of commands that run one after another. So far, each of your programs had one thread: the one that runs Main.

The threads in one process share their memory: they see the same objects. That makes them handy (they can easily pass data to each other) and dangerous (they can mess each other up, as you will see in a moment).

  • Parallel means really at the same time, on different cores of the processor.
  • Concurrent means on one core, taking turns so fast that it looks like the same time.

For the programmer the difference rarely matters: in both cases you do not know which thread will go on when.

More precisely, if you are curious
  • The Chrome browser starts a separate process for each tab. That is why one frozen site does not stop the others, and also why the browser uses a lot of memory.
  • Every program that does several things at once uses threads: a game plays the sound in one thread and runs the logic in another; a web server serves each client in its own thread.
  • The price: each thread has its own stack, and the memory for it is set aside separately. Thousands of threads cost a lot. That is why real programs use a thread pool (the last idea in the lesson).

See it

The first thread

The Thread class is a thread. You pass its constructor what to do: a ThreadStart delegate, a method with no parameters that returns nothing. The shortest way is with a lambda:

Thread worker = new Thread(() => Download("april.pdf", 200));
worker.Start();
Console.WriteLine("Main does not wait");
worker.Join();
Console.WriteLine("The report is downloaded");
Output
Main does not wait
Downloaded april.pdf
The report is downloaded
  • Start() starts the new thread and returns right away. Main goes on without waiting, so "Main does not wait" is printed first.
  • Join() makes Main wait for the thread to finish.

Download is the method from the first card. Exceptions in C# are not checked, so there is no throws here and nothing has to be caught: Main stays static void Main().

More precisely, if you are curious
The lambda is the shortest way, but not the only one. In older code you will come across the other three:
class ReportJob
{
public void Run()
{
Console.WriteLine("2. a method of an object");
}
}
static void PrintReport()
{
Console.WriteLine("1. a static method by name");
}
static void Main()
{
Thread a = new Thread(PrintReport);
Thread b = new Thread(new ReportJob().Run);
Thread c = new Thread(delegate ()
{
Console.WriteLine("3. an anonymous method");
});
Thread d = new Thread(() => Console.WriteLine("4. a lambda"));
foreach (Thread t in new Thread[] { a, b, c, d })
{
t.Start();
t.Join();
}
}
Output
1. a static method by name
2. a method of an object
3. an anonymous method
4. a lambda
In C#, Thread cannot have a subclass: the class is sealed. There are no anonymous classes either; their old counterpart is the anonymous method delegate () { ... }. So the task is always kept apart from the thread: a delegate that can also be given to a thread pool. If the thread needs an argument, there is also ParameterizedThreadStart: new Thread(file => ...) and Start("march.pdf"). The argument there is an object, so a lambda that captures a variable is handier.

See it

Three at once

Now the three reports, each in its own thread:
long start = Environment.TickCount64;
Thread march = new Thread(() => Download("march.pdf", 600));
Thread april = new Thread(() => Download("april.pdf", 200));
Thread may = new Thread(() => Download("may.pdf", 400));
march.Start();
april.Start();
may.Start();
march.Join();
april.Join();
may.Join();
long ms = Environment.TickCount64 - start;
Console.WriteLine($"Done in under a second: {ms < 1000}");
Output
Downloaded april.pdf
Downloaded may.pdf
Downloaded march.pdf
Done in under a second: True

The threads start in the order March, April, May, but they finish in the order of the pauses: April (0.2 s), May (0.4 s), March (0.6 s). The whole wait is as long as the longest one, 0.6 seconds instead of 1.2.

The operating system decides the order in which the threads reach WriteLine. Here the pauses are different enough, so the order is the same. If they were equal, it could be different on every run.Environment.TickCount64 is the number of milliseconds since the computer started: the difference between two readings is how much time has passed.

Try it

Start or a call

What will the program print? (Name is the name of the thread)

Thread.CurrentThread.Name = "Main";
ThreadStart job = () => Console.WriteLine($"Running in: {Thread.CurrentThread.Name}");
Thread t = new Thread(job);
t.Name = "downloader";
job();
t.Start();
t.Join();
Answer

The idea

The life of a thread

A thread goes through several states, and ThreadState tells you which one it is in:
Thread t = new Thread(() => Download("may.pdf", 500));
Console.WriteLine(t.ThreadState);
t.Start();
Thread.Sleep(100);
Console.WriteLine(t.ThreadState);
t.Join();
Console.WriteLine(t.ThreadState);
try
{
t.Start();
}
catch (ThreadStateException)
{
Console.WriteLine("Second Start: ThreadStateException");
}
Output
Unstarted
WaitSleepJoin
Downloaded may.pdf
Stopped
Second Start: ThreadStateException
State When
Unstarted created, not started with Start() yet
Running running or ready to run
WaitSleepJoin waiting: for a lock, for another thread (Join), or for a set time (Sleep)
Stopped its method has finished

A thread can be started only once. A second Start() on the same thread throws ThreadStateException. For a shorter answer, just "is it running", there is IsAlive.

More precisely, if you are curious

More about Thread:

  • Interrupt() asks the thread to stop. If it is sleeping or waiting, Sleep/Join throws ThreadInterruptedException. In C# it is not checked: if the thread does not catch it, the program stops with an error. Stopping a thread from outside with Abort() does not work in today's .NET.
  • Name, ManagedThreadId: the name and the number of the thread. Thread.CurrentThread gives you the thread the code is running in.
  • Thread.Yield() hints that the thread can give the processor to another one. You rarely need it.

Problem

The lost visits

The site counts its visits. Two threads serve visitors, and each one calls Increment() 100,000 times:
Program.cs
class Counter
{
private int count = 0;
public void Increment()
{
count++;
}
public int Count => count;
}
static void Main()
{
Counter visits = new Counter();
ThreadStart work = () =>
{
for (int i = 0; i < 100_000; i++)
{
visits.Increment();
}
};
Thread first = new Thread(work);
Thread second = new Thread(work);
first.Start();
second.Start();
first.Join();
second.Join();
Console.WriteLine($"Visits: {visits.Count}");
}

You expect 200,000. Five runs:

Output (different every time)
Visits: 146528
Visits: 120201
Visits: 178089
Visits: 152070
Visits: 153205

Less every time, and different every time. The reason is that count++ is not one step, but three:

int value = count; // 1. read
value = value + 1; // 2. add 1
count = value; // 3. write

class Counter
{
private int count = 5;
private readonly object sync = new object();
public void Increment()
{
count++; // read, add 1, write
}
public void SafeIncrement()
{
lock (sync)
{
count++;
}
}
public int Count => count;
}
Step by step, in words
  1. count is 5. Threads A and B call Increment() at the same time.
  2. A reads count: 5.
  3. B reads count before A has written: also 5.
  4. A adds 1 in its own copy and writes 6.
  5. B writes 6 too. Two calls, and count is 6, not 7: one of the additions is lost.
  6. Once more, now with lock: A takes the lock, reads 5 and writes 6.
  7. B reaches lock (sync) and waits until A releases the lock.
  8. B takes the lock, reads 6 and writes 7: nothing is lost.

When both threads read the same value before either of them has written, one of the additions is lost. This is called a race condition. Whether it happens depends on when the operating system switches the threads, so the result is different on every run.

The idea

lock: one at a time

The solution is for only one thread at a time to do these three steps. The lock block does exactly that:
class Counter
{
private int count = 0;
private readonly object sync = new object();
public void Increment()
{
lock (sync)
{
count++;
}
}
public int Count
{
get
{
lock (sync)
{
return count;
}
}
}
}
Output
Visits: 200000

sync is an ordinary object that serves only as a key: any object in C# can be a lock. The thread takes it when it enters lock (sync) and releases it when it leaves, even on an exception. The other thread that wants the same key waits at the entrance. Now the result is 200,000 on every run. Reading Count is in a lock too: that way the thread that reads sees the last value written.

The key is a private field, not this: that way no code from outside can lock the same object and make you wait. C# has no synchronized word in front of a method like Java. The closest thing is the attribute [MethodImpl(MethodImplOptions.Synchronized)], but it locks the whole object (this), so it is not recommended:

using System.Runtime.CompilerServices;
class Counter
{
private int count = 0;
[MethodImpl(MethodImplOptions.Synchronized)]
public void Increment()
{
count++;
}
public int Count => count;
}

For a counter there is also a ready-made class: Interlocked. Interlocked.Increment(ref count) does the three steps at once, without a lock. ref passes the field itself, not a copy of its value:

class Counter
{
private int count = 0;
public void Increment()
{
Interlocked.Increment(ref count);
}
public int Count => count;
}
Output
Visits: 200000

Here Count reads without protection, because Main reads it only after Join. Join guarantees that it sees everything the thread has written.

More precisely, if you are curious
  • Monitor.Wait and Monitor.PulseAll: a thread in a lock block can release the lock and go to sleep until another thread wakes it up. That way the kitchen waits for an order, and the checkout waits for the kitchen to take the previous one (the "producer and consumer" problem):
class OrderBox
{
private string order;
private readonly object sync = new object();
public void Put(string newOrder)
{
lock (sync)
{
while (order != null)
{
Monitor.Wait(sync);
}
order = newOrder;
Monitor.PulseAll(sync);
}
}
public string Take()
{
lock (sync)
{
while (order == null)
{
Monitor.Wait(sync);
}
string result = order;
order = null;
Monitor.PulseAll(sync);
return result;
}
}
}
static void Main()
{
OrderBox box = new OrderBox();
Thread kitchen = new Thread(() =>
{
for (int i = 0; i < 3; i++)
{
Console.WriteLine($"The kitchen took {box.Take()}");
}
});
kitchen.Start();
box.Put("pizza");
box.Put("salad");
box.Put("soup");
kitchen.Join();
}
Output
The kitchen took pizza
The kitchen took salad
The kitchen took soup

Monitor.Wait is in a while, not in an if: the thread can wake up for no reason, so it checks the condition again. BlockingCollection<string> with boundedCapacity: 1 does the same out of the box: Add waits if it is full, and Take waits if it is empty.

  • Deadlock: two threads wait for each other forever. The office holds the printer and waits for the scanner, and the front desk holds the scanner and waits for the printer:
object printer = new object();
object scanner = new object();
Thread office = new Thread(() =>
{
lock (printer)
{
Thread.Sleep(100);
lock (scanner) { Console.WriteLine("The office is copying"); }
}
});
Thread reception = new Thread(() =>
{
lock (scanner)
{
Thread.Sleep(100);
lock (printer) { Console.WriteLine("The front desk is copying"); }
}
});

The program hangs and never prints anything. The way to avoid it is for all the threads to take the locks in the same order.

  • Priorities and background threads: Priority hints how important the thread is, but the operating system may ignore it, so do not rely on it for the program to be correct. IsBackground = true makes the thread a background thread: the program ends without waiting for it:
Thread report = new Thread(() => Download("annual.pdf", 100));
report.Priority = ThreadPriority.Lowest; // Lowest, BelowNormal, Normal, AboveNormal, Highest
report.IsBackground = true; // the program does not wait for background threads
Console.WriteLine($"{report.Priority}, background: {report.IsBackground}");
Output
Lowest, background: True
  • Ready-made collections for many threads are in System.Collections.Concurrent: ConcurrentDictionary, ConcurrentQueue, BlockingCollection. Their methods do the check and the change at once, without your own lock.

Try it

Two ATMs

Two people each withdraw €80 from one account with €100 at the same moment. Sometimes both of them get the money, and the balance becomes -€60. Which line is the bug on?

Pick the line with the bug.

The idea

Thread pool and Task

Making a thread for every task is expensive, and with a thousand tasks it is dangerous too. A thread pool is a few threads that take tasks from a queue. In .NET there is one for the whole program, ThreadPool, and it decides by itself how many threads to have. Task.Run (from System.Threading.Tasks) gives it a task:
static int SizeOf(string file, int ms)
{
Thread.Sleep(ms);
return file.Length * 100;
}
string[] files = { "march.pdf", "april.pdf", "may.pdf", "june.pdf" };
List<Task<int>> sizes = new List<Task<int>>();
foreach (string file in files)
{
sizes.Add(Task.Run(() => SizeOf(file, 300)));
}
for (int i = 0; i < files.Length; i++)
{
Console.WriteLine($"{files[i]}: {sizes[i].Result} KB");
}
Output
march.pdf: 900 KB
april.pdf: 900 KB
may.pdf: 700 KB
june.pdf: 800 KB
  • Task.Run takes a task that returns a result, gives it to the pool and right away returns a Task<int>: a promise that the result will come (in Java this is a Future).
  • .Result waits for the task to finish and returns the result. If the task threw an exception, .Result throws it, wrapped in an AggregateException.
  • The pool is not shut down at the end: its threads are background threads, so the program ends when Main ends. A task that is still running at that point is simply cut off.

The pool decides how many tasks run at once. If you want to limit them, like a pool of 2 threads, Parallel.For runs the body for each i in the pool, at most MaxDegreeOfParallelism at a time, and waits for all of them:

string[] files = { "march.pdf", "april.pdf", "may.pdf", "june.pdf" };
int[] sizes = new int[files.Length];
ParallelOptions options = new ParallelOptions();
options.MaxDegreeOfParallelism = 2;
Parallel.For(0, files.Length, options, i =>
{
sizes[i] = SizeOf(files[i], 300);
});
for (int i = 0; i < files.Length; i++)
{
Console.WriteLine($"{files[i]}: {sizes[i]} KB");
}
Output
march.pdf: 900 KB
april.pdf: 900 KB
may.pdf: 700 KB
june.pdf: 800 KB

.Result waits by keeping the thread busy. In the web module you will see await, which waits for the same task without taking up a thread.

More precisely, if you are curious
If the task does not return anything, Task.Run gives you a Task with no type. Then you wait for all of them with Task.WaitAll:
List<Task> jobs = new List<Task>();
for (int i = 1; i <= 5; i++)
{
string file = $"report-{i}.pdf";
jobs.Add(Task.Run(() => Download(file, 100)));
}
Task.WaitAll(jobs.ToArray());
Console.WriteLine("All done.");
Output of one run
Downloaded report-2.pdf
Downloaded report-1.pdf
Downloaded report-4.pdf
Downloaded report-5.pdf
Downloaded report-3.pdf
All done.
The five reports run at the same time (the pool has enough threads), so their order changes from run to run. The older way is ThreadPool.QueueUserWorkItem, but it does not give you a task to wait for.

Try it

Which result comes first?

"archive.zip" takes 0.6 s to download, and "note.txt" takes 0.1 s. What will the program print?

Task<int> big = Task.Run(() => SizeOf("archive.zip", 600));
Task<int> small = Task.Run(() => SizeOf("note.txt", 100));
Console.WriteLine($"Big: {big.Result}");
Console.WriteLine($"Small: {small.Result}");
Answer

Exercise

Programs with threads

Solve these in your editor.

1. Parallel sum. The array sales has 1,000,000 sales, sales[i] = i % 100. Split it into 4 equal parts and sum each one in its own thread. Each thread writes to its own place in the array long[] partial, so you do not need synchronization. A lambda in a for captures the loop variable itself, not its value (the lesson "Lambdas"): copy the number of the part into a new variable before the lambda. Wait for them with Join and add up the parts:

Output
Parts: [12375000, 12375000, 12375000, 12375000]
Total: 49500000

2. Downloading with a pool. Download six reports (march.pdf, april.pdf, may.pdf, june.pdf, july.pdf, august.pdf) with Parallel.For, 3 at a time. Use SizeOf(file, 300) from the pool card. Print the sizes in the order of the files, the total, and whether it all took under 1.5 seconds (one after another would be 1.8 s):

Output
march.pdf: 900 KB
april.pdf: 900 KB
may.pdf: 700 KB
june.pdf: 800 KB
july.pdf: 800 KB
august.pdf: 1000 KB
Total: 5100 KB
Under 1.5 seconds: True

3. A counter without losses. Four threads, each with 50,000 visits, increase a shared counter. Write the counter so that the result is always 200,000. First try it without protection and see how much is lost, then fix it once with lock and once with Interlocked.

Output
Visits: 200000

More tasks

  • A test for the counter. Write an xUnit test (the lesson "Tests") that starts 8 threads with 100,000 increases each and checks for 800,000 with Assert.Equal. Run it against the unprotected counter a few times, then against the counter with lock. A test that fails only sometimes is the typical sign of a race condition. With 10,000 per thread in .NET, a thread often finishes before the next one has started, and the test passes: the race is there, but you do not see it.
  • The deadlock. Run the example with the printer and the scanner from "More precisely" (with Start and Join) and watch it hang. Fix it so that both threads take the printer first.

Test

Check yourself

1. What is the difference between t.Start() and job(), when job is the delegate of the thread?
2. Why does Main call Join() before it prints the result?
3. Two threads each do count++ 100,000 times on a shared field without synchronization. What will count be at the end?
4. What does lock (sync) { ... } guarantee?
5. You start three tasks with Task.Run and keep a Task<int> for each one. What does task.Result do?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·