π The Restaurant Kitchen: A Story About Multithreading
Imagine you own a busy restaurant. You have ONE chef (your computerβs single thread). When 10 orders come in at once, that poor chef has to cook each dish one by one. Customers wait forever!
What if you hired MORE chefs? Now multiple dishes cook at the same time. Thatβs multithreading β having multiple workers (threads) doing tasks simultaneously.
π§΅ Thread Basics: Your First Kitchen Helper
A Thread is like hiring a new chef. Each chef can work independently.
Creating Your First Thread
// Hire a new chef (create a thread)
Thread chef = new Thread(() => {
Console.WriteLine("Chef is cooking!");
});
// Tell the chef to start working
chef.Start();
// Wait for chef to finish
chef.Join();
What happens here?
new Thread(...)β We hire a chefStart()β Chef begins cookingJoin()β We wait until chef finishes
Real Example: Two Chefs Working
Thread chef1 = new Thread(() => {
for (int i = 0; i < 3; i++) {
Console.WriteLine("Chef 1 cooking...");
Thread.Sleep(100); // Takes time
}
});
Thread chef2 = new Thread(() => {
for (int i = 0; i < 3; i++) {
Console.WriteLine("Chef 2 cooking...");
Thread.Sleep(100);
}
});
chef1.Start();
chef2.Start();
Output might be:
Chef 1 cooking...
Chef 2 cooking...
Chef 1 cooking...
Chef 2 cooking...
Notice how they work at the same time? Thatβs the magic!
π ThreadPool: The Smart Staffing Agency
Hiring and firing chefs takes time. What if you had a staffing agency that keeps chefs ready?
Thatβs the ThreadPool β a pool of pre-made threads ready to work!
// Ask the agency for a chef
ThreadPool.QueueUserWorkItem(_ => {
Console.WriteLine("Agency chef cooking!");
});
// Ask for another
ThreadPool.QueueUserWorkItem(_ => {
Console.WriteLine("Another agency chef!");
});
Why Use ThreadPool?
| Manual Threads | ThreadPool |
|---|---|
| You hire each chef | Agency provides chefs |
| Slow to create | Already ready |
| You manage them | Agency manages them |
| Good for long tasks | Good for quick tasks |
Simple Rule: Use ThreadPool for small, quick jobs. Use regular Threads for long, important tasks.
π Synchronization Primitives: The Kitchen Rules
Problem: Two chefs reach for the same pan at once. Disaster!
We need rules (synchronization primitives) to prevent chaos.
graph TD A["Two Threads"] --> B{Same Resource?} B -->|Yes| C["Need Synchronization!"] B -->|No| D["Safe - No Problem"] C --> E["Use Lock/Monitor/Mutex"]
π Lock vs Monitor vs Mutex: Three Types of Kitchen Locks
1. Lock β The Simple Door Lock
Like a bathroom door lock. One person at a time.
private object _lock = new object();
private int counter = 0;
void SafeIncrement() {
lock (_lock) {
// Only ONE thread here at a time
counter++;
}
}
2. Monitor β Lock with More Control
Same lock, but you can wait and signal others.
private object _lock = new object();
void WaitForFood() {
lock (_lock) {
// Wait until food is ready
Monitor.Wait(_lock);
Console.WriteLine("Food received!");
}
}
void FoodIsReady() {
lock (_lock) {
// Tell waiting person
Monitor.Pulse(_lock);
}
}
3. Mutex β The Master Key
Works across different programs. Like a key that works on multiple buildings.
using Mutex mutex = new Mutex(false, "MyAppMutex");
mutex.WaitOne(); // Get the key
try {
// Only ONE program can be here
Console.WriteLine("I have the key!");
}
finally {
mutex.ReleaseMutex(); // Return the key
}
Quick Comparison
| Feature | lock | Monitor | Mutex |
|---|---|---|---|
| Simple to use | β Yes | Medium | Complex |
| Cross-process | β No | β No | β Yes |
| Wait/Signal | β No | β Yes | β No |
| Speed | Fast | Fast | Slower |
Rule of Thumb:
- 99% of time: Use
lock - Need wait/signal: Use
Monitor - Multiple programs: Use
Mutex
β οΈ Race Conditions: When Chefs Collide
A race condition is when the result depends on who finishes first.
The Problem
int balance = 100;
// Thread 1: Withdraw 50
void Withdraw() {
if (balance >= 50) {
Thread.Sleep(1); // Tiny delay
balance -= 50;
}
}
// Thread 2: Also withdraw 50
// BOTH might withdraw!
What could happen:
- Thread 1 checks: balance is 100 β
- Thread 2 checks: balance is 100 β
- Thread 1 withdraws: balance = 50
- Thread 2 withdraws: balance = 0
Both withdrew, but we only had 100! πΈ
The Fix
private object _lock = new object();
void SafeWithdraw() {
lock (_lock) {
if (balance >= 50) {
balance -= 50;
}
}
}
Now only ONE thread can check AND withdraw at a time.
π¦ Concurrent Collections: Thread-Safe Containers
Normal collections are NOT safe with multiple threads.
// DANGEROUS - Don't do this!
List<int> list = new List<int>();
// Multiple threads adding = CRASH!
Safe Alternatives
ConcurrentDictionary β Thread-safe dictionary
var dict = new ConcurrentDictionary<string, int>();
dict.TryAdd("apples", 5);
dict.AddOrUpdate("apples", 1, (k, v) => v + 1);
ConcurrentQueue β Thread-safe queue
var queue = new ConcurrentQueue<string>();
queue.Enqueue("Order 1");
queue.TryDequeue(out string order);
ConcurrentBag β Thread-safe unordered collection
var bag = new ConcurrentBag<int>();
bag.Add(1);
bag.Add(2);
bag.TryTake(out int item);
When to Use What
| Collection | Use When |
|---|---|
| ConcurrentDictionary | Key-value lookups |
| ConcurrentQueue | First-in-first-out |
| ConcurrentStack | Last-in-first-out |
| ConcurrentBag | Order doesnβt matter |
| BlockingCollection | Producer-consumer pattern |
π Parallel.For and Parallel.ForEach: Turbo Mode!
Want to process a list with ALL your chefs at once?
Regular For Loop (One Chef)
// One chef, one dish at a time
for (int i = 0; i < 100; i++) {
ProcessItem(i);
}
Parallel.For (All Chefs!)
// ALL chefs cooking at once!
Parallel.For(0, 100, i => {
ProcessItem(i);
});
Parallel.ForEach Example
string[] dishes = { "Pizza", "Pasta", "Salad" };
// Process all dishes simultaneously
Parallel.ForEach(dishes, dish => {
Console.WriteLine(quot;Cooking {dish}");
Thread.Sleep(1000); // Each takes 1 second
});
// Total time: ~1 second (not 3!)
When to Use Parallel
graph TD A["Do I have many items?"] -->|Yes| B["Is each item independent?"] A -->|No| C["Use regular loop"] B -->|Yes| D["Is the work CPU-heavy?"] B -->|No| C D -->|Yes| E["Use Parallel!"] D -->|No| F["Maybe - test it"]
Important Rules
- Items must be independent β No chef should need another chefβs dish
- CPU-bound work β Good for calculations, NOT for waiting (use async for that)
- Test performance β Sometimes regular loops are faster for small data!
π― Summary: Your Multithreading Toolkit
| Tool | Purpose | Example Use |
|---|---|---|
| Thread | Manual worker | Long background tasks |
| ThreadPool | Quick workers | Short, quick jobs |
| lock | Simple protection | Protect shared data |
| Monitor | Advanced protection | Wait/signal patterns |
| Mutex | Cross-program lock | Single app instance |
| Concurrent Collections | Safe containers | Shared lists/dictionaries |
| Parallel.For/ForEach | Parallel processing | Process large datasets |
Golden Rules
- Start simple β Use
lockbefore trying fancier stuff - Avoid sharing β If threads donβt share data, no problems!
- Test thoroughly β Race conditions are sneaky
- Use Concurrent Collections β Donβt make regular collections βsafeβ
- Parallel isnβt always faster β Measure before assuming!
π You Did It!
You now understand:
- β How threads work (the chefs)
- β ThreadPool (the staffing agency)
- β Synchronization (the kitchen rules)
- β Lock vs Monitor vs Mutex (types of locks)
- β Race conditions (when chefs collide)
- β Concurrent collections (safe containers)
- β Parallel loops (turbo mode!)
Remember: Multithreading is like managing a restaurant kitchen. With the right tools and rules, your kitchen runs smoothly. Without them? Chaos! π³
Now go build something awesome! π
