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
Thread.Sleep(ms) stops the program for that many milliseconds. Thread is in System.Threading, which is included by default.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");}Downloaded march.pdfDownloaded april.pdfDownloaded may.pdfDoneThe 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");Main does not waitDownloaded april.pdfThe report is downloadedStart()starts the new thread and returns right away.Maingoes on without waiting, so "Main does not wait" is printed first.Join()makesMainwait 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
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(); }}1. a static method by name2. a method of an object3. an anonymous method4. a lambdaThread 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
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}");Downloaded april.pdfDownloaded may.pdfDownloaded march.pdfDone in under a second: TrueThe 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 reachWriteLine. 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();job() is an ordinary call of the delegate: the lambda runs right away, in the thread of Main. There is no new thread. Only t.Start() starts the thread "downloader", which runs the same lambda. The main thread in .NET has no name, so the first line gives it "Main". Thread in C# has no Run() method to mix up with Start(), but the same mistake is to call the method yourself instead of giving it to a thread: the program works, only nothing runs in parallel.The idea
The life of a thread
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");}UnstartedWaitSleepJoinDownloaded may.pdfStoppedSecond 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/JointhrowsThreadInterruptedException. In C# it is not checked: if the thread does not catch it, the program stops with an error. Stopping a thread from outside withAbort()does not work in today's .NET.Name,ManagedThreadId: the name and the number of the thread.Thread.CurrentThreadgives 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
Increment() 100,000 times: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:
Visits: 146528Visits: 120201Visits: 178089Visits: 152070Visits: 153205Less every time, and different every time. The reason is that count++ is not one step, but three:
int value = count; // 1. readvalue = value + 1; // 2. add 1count = value; // 3. writeclass 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
- count is 5. Threads A and B call Increment() at the same time.
- A reads count: 5.
- B reads count before A has written: also 5.
- A adds 1 in its own copy and writes 6.
- B writes 6 too. Two calls, and count is 6, not 7: one of the additions is lost.
- Once more, now with lock: A takes the lock, reads 5 and writes 6.
- B reaches lock (sync) and waits until A releases the lock.
- 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
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; } } }}Visits: 200000sync 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;}Visits: 200000Here 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.WaitandMonitor.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();}The kitchen took pizzaThe kitchen took saladThe kitchen took soupMonitor.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:
Priorityhints 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 = truemakes 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, Highestreport.IsBackground = true; // the program does not wait for background threadsConsole.WriteLine($"{report.Priority}, background: {report.IsBackground}");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 ownlock.
Try it
Two ATMs
Pick the line with the bug.
The check balance >= amount and the withdrawal are not one whole. The two threads check at the same time, both see 100 and both withdraw. In this small example it happens rarely, which makes the bug sneaky: it passes a thousand times and fails at the customer's. The fix is a lock around the check and the withdrawal (and around reading Balance):
class Account{ private int balance = 100; private readonly object sync = new object();
public void Withdraw(int amount) { lock (sync) { if (balance >= amount) { balance -= amount; } } }
public int Balance { get { lock (sync) { return balance; } } }}Then the second ATM waits for the first one, sees €20 and refuses. The balance stays €20.
The idea
Thread pool and Task
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");}march.pdf: 900 KBapril.pdf: 900 KBmay.pdf: 700 KBjune.pdf: 800 KBTask.Runtakes a task that returns a result, gives it to the pool and right away returns aTask<int>: a promise that the result will come (in Java this is aFuture)..Resultwaits for the task to finish and returns the result. If the task threw an exception,.Resultthrows it, wrapped in anAggregateException.- The pool is not shut down at the end: its threads are background threads, so the program ends when
Mainends. 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");}march.pdf: 900 KBapril.pdf: 900 KBmay.pdf: 700 KBjune.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
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.");Downloaded report-2.pdfDownloaded report-1.pdfDownloaded report-4.pdfDownloaded report-5.pdfDownloaded report-3.pdfAll done.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}");The small file really is ready first. But big.Result is the first line, and it waits for the big one. When that one comes, small.Result returns right away, because the result is already there. The order of the output is the order of the .Result reads, so it is the same on every run.
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:
Parts: [12375000, 12375000, 12375000, 12375000]Total: 495000002. 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):
march.pdf: 900 KBapril.pdf: 900 KBmay.pdf: 700 KBjune.pdf: 800 KBjuly.pdf: 800 KBaugust.pdf: 1000 KBTotal: 5100 KBUnder 1.5 seconds: True3. 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.
Visits: 200000More 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 withlock. 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
StartandJoin) and watch it hang. Fix it so that both threads take the printer first.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: try, catch and finally · Lists · Lambdas and delegates · Tests with xUnit
- Continues in: Network programming · async and await · WebAPI: data for other programs · Quick sort and the bad pivot
- Tasks on this topic: Real-world programs