3 C# Threading crimes I committed (so you don't have to)

My restaurant's chef froze every time he chopped vegetables. So today we learn C# threading from zero: what a thread is, why your program freezes, and how to do two things at once without the whole kitchen catching fire. Threads are cooks. Your program starts with one, the main thread: the Head Chef at the pass. If he walks off to chop onions, nobody gets served. Hire a prep cook, and the kitchen keeps running… until they share a plate. 🍽️ Three crimes I committed so you don't have to: - "Collection was modified; enumeration operation may not execute" (two threads, one list) - 100,000 + 100,000 ≠ 200,000 (race condition on ++) - await froze my main loop again (await means WAIT!) What we cover: - What a thread is, and why the main thread matters - Why a program's main loop freezes - new Thread().Start() and Join() - Race conditions and lock - Task.Run and the thread pool - What async/await actually does - IsCompleted and .Result (and when .Result freezes you) - Running two tasks at the same time - The main-thread rule in Windows Forms, WPF, Godot and Unity 🌍 Subtitles available in English, Português (Portugal) and Português (Brasil). -------------------------------------------------------------------------------------------------------------- CHAPTERS -------------------------------------------------------------------------------------------------------------- 00:00 Welcome to Chez Léo 00:36 Tonight's menu 00:49 The kitchen: what is a thread? 01:44 Building the console kitchen (main loop) 03:02 Why the program freezes 03:57 Hiring a prep cook (new Thread) 04:53 Crash #1: Collection was modified 06:33 Fix: give each cook their own plate 08:34 Race conditions: the board that can't count 12:34 Fix: one marker (lock) 13:56 The button masher 14:36 Tasks and the thread pool 15:03 Editor Leo: what await actually is 16:39 Crash #3: await means WAIT 17:39 Fix: keep the ticket (IsCompleted & Result) 20:06 Two orders at once (sauce!) 24:27 Who brings the plate back? 26:38 The main-thread rule (WinForms, WPF, Godot, Unity) 27:59 Recap: the three crimes 28:42 Class dismissed Subscribe for more! Like & subscribe — it helps keep the lab open. -------------------------------------------------------------------------------------------------------------- MUSIC USED -------------------------------------------------------------------------------------------------------------- ► Music Credit: Yakuzee Beatz Track Name: "Soul Aches" Music By: Yakuzee Beatz https://soundcloud.com/user-811451507 Official Yakuzee Beatz YouTube Channel HERE: @yakuzeebeatz6536 License for commercial use: Creative Commons Attribution 3.0 Unported (CC BY 3.0) License. Music promoted by NCM https://goo.gl/fh3rEJ ► Music Credit: Artificial.Music Track Name: "And So It Begins" Music By: Artificial Music https://soundcloud.com/artificial-music YT Channel HERE: @smarttoaster Original upload HERE - https://soundcloud.com/artificial-music/and-so-it-begins License for commercial use: Creative Commons Attribution 3.0 Unported (CC BY 3.0) License. Music promoted by NCM https://goo.gl/fh3rEJ ► Music Credit: Artificial.Music Track Name: "Talking With You" Music By: Artificial Music https://soundcloud.com/artificial-music YT Channel HERE: @smarttoaster Original upload HERE - https://soundcloud.com/artificial-music/withyou License for commercial use: Creative Commons Attribution 3.0 Unported (CC BY 3.0) License. Music promoted by NCM https://goo.gl/fh3rEJ -------------------------------------------------------------------------------------------------------------- SOCIALS -------------------------------------------------------------------------------------------------------------- Watch the chaos live on Twitch: https://twitch.tv/professorleo Join the lab's Discord Server: https://discord.gg/Gp87EyBDvD Clips Channel: https://www.youtube.com/@LeoOffHours Class dismissed. 🎓 #csharp #programming #dotnet

Transcript

Bonsoir, welcome to Chez Léo, the finest restaurant ever to run inside a terminal window. May I present the menu? Tonight, I recommend the house special, Thread Salad. Our chef will begin chopping the vegetables. One moment, s'il vous pl— No, no, your internet is fine. That's my kitchen. The chef is frozen and the clock stopped. Oh, he's back. The clock just skipped 5 seconds like nothing happened. Why does this keep happening?

Today, we're gonna learn about threading, why your program freezes, how to make two things happen at once, and the three ways I immediately broke it. Including one where the computer can't add 1 plus 1. You'll see. Picture a restaurant kitchen. The boss is a head chef. He stands at the pass. The counter between the kitchen and the dining room. New orders come in. He reads them. Finished plates go out. He checks them and sends them.

His whole job is to keep the pass moving. That's your program. The head chef is a thread. A thread is a worker that reads your code line by line, top to bottom. And every program starts with exactly one. The main thread. Your head chef. Now, what if he's alone in the kitchen and the dish needs 5 minutes of chopping vegetables? He walks away from the pass and starts shopping. New orders pile up. Plates sit there going cold.

Nobody gets served. Your head chef just became the prep cook. That's the freeze. Remember it. It's coming back. More than once. Chez Léo is a tiny console kitchen. And it's built like a lot of real programs. A main loop. That checks for input. Updates the screen. Waits a moment. And repeats forever. Any app with a window has a loop like this hiding inside it. In kitchen terms, the head chef working the pass. Orders in, plates out, repeat.

Console.KeyAvailable asks, did someone press a key? Important. It doesn't wait. If nobody press anything, it says no and we keep going. Then we draw a spinner. That changes every loop. We draw the current time and our chopped vegetables. Thread.Sleep pauses for 5 tenths of a second. So the screen updates 2 times per second. The spinner and the clock are our heartbeat. If they move, the head chef is at the pass. And the prep work at the bottom.

100 chops. One hashtag for each piece. The Thread.Sleep is pretend that hard work is happening. In a real program, this would be resizing photos, reading big files, crunching numbers. 50 milliseconds times 100 chops is 5 seconds. There it is. Our main thread is the head chef. And ChopVegetables is chopping vegetables. Very subtle metaphor. When he hits that line, he leaves the pass. Walks over to the cutting board and doesn't come back until all 100 chops are done.

During those 5 seconds, nobody checks the keyboard. Nobody updates the screen. The clock can't tick. Because the only person who could update it is holding the knife. Head chef. Prep cook now. And as you can see, while it was frozen, I mashed the C again multiple times and the key presses didn't vanish. They piled up like order tickets and as soon as the head chef got back to the pass, the next ticket said chopped vegetables and he went straight back to the cutting board.

Over and over until all the tickets were gone. The fix? Simple. Hire a prep cook. So, this involves a simple change. In here, instead of calling ChopVegetables, we're gonna do it like this. Delete this. New Thread. And then ChopVegetables inside .Start(). This is what we call to hire a new prep cook. And hand him the ChopVegetables recipe. And here is the magic. The head chef doesn't wait. He hands over the recipe and goes straight back to the pass.

Two cooks, two things at once. And new Thread means new. Every time this line runs, a brand new cook gets hired. Remember that. Future Leo is going to regret it. It's alive. It's spinning. The veggies are getting chopped while it spins. One Thread and Butter coming right up. Now, for a real struggle. Imagine if the code would be behaving faster. Say for example, here, instead of waiting for 5 tenths of a second. What if I waited for 1 tenth of a second?

And in here, imagine if every piece would take 1 millisecond to draw. So, Thread.Sleep(1). And now if I open this and say for example, I spam the button. There we go. System.InvalidOperationException. The collection was modified. The enumeration operation may not execute. Let's translate this. So, collection is our list of vegetables. Enumeration is a foreach that is walking through the list one item at a time. So, this means someone changed the list while I was walking through it.

Look at what we built. The head chef is reading the veggies list, top to bottom, to draw it. At the exact same time the prep cook is adding veggies to that same list. The head chef is sending the plate out of the pass while the prep cook is still piling food onto it. C#'s list notices it changed mid-read and instead of showing garbage it panics and stops the program. Honestly, I respect that. And the worst part, sometimes it doesn't crash.

It depends on whether the two cooks happen to touch the list at the exact same moment. Pure timing. Pure luck. A bug that only shows up sometimes is the scariest bug in programming. The fix? Give the prep cook his own plate. He builds it at his own station and only when it's completely finished, he puts it on the pass. And while we're here, a progress number so we can watch him work. And now we're gonna create a new variable that's gonna be called chopProgress.

Console.WriteLine. ChoppingProgress. And chopProgress. Percentage. Then, down here in the method itself, instead of doing this, let's just say list of string myPlate. Or let's just call it plate. Is equal to. A new. And in here instead of adding to veggies, let's add to plate. And increment the chopProgress. Or in this case, say it's i plus one. And at the end of this for loop, let's just say veggies equals plate. This way, nobody else ever touches the vegetables while he's working.

That's his plate. The head chef keeps drawing the old list which nobody is changing. And at the very end, one line and plate's done on the pass. If I run this, as you can see the chopping progress is incrementing. But the veggies isn't because it only does that now. Although now it has a simple bug. But we'll fix that eventually. As you can see, no crash. It's sort of smooth. And here is the whole secret of threading in one sentence.

Threads aren't the danger. Sharing is. And now I can hear you. Leo, come on. That was a whole list. Surely tiny things are safe to share. Okay, let's share the tiniest thing possible. One number. Every kitchen has a board counting how many dishes went out tonight. Two cooks both sending out dishes, both updating the board. So, let's create a new method that's going to send out dishes. So, let's add new integer. That's gonna be dishesOut.

Start at zero. And let's create the void down here. void SendOutDishes. And let's say in here, I'm gonna wait for a new key. Let's call it maybe D. And it's gonna do two things. I'm gonna have two concurrent threads. For two cooks, basically. So, I'm gonna say, Thread cookA equals new Thread SendOutDishes. I'm gonna start. Oh, wait. No, no, no. Just that. We are gonna start them in simultaneous way. So, it's gonna be after this.

I'm gonna have cook A and cook B. So, let's start them. cookA.Start(). cookB.Start(). And we're gonna join them. Okay. And finally, we're gonna say, down here. After everything here. Console.WriteLine. Dishes out. Okay. And in here, in this void, we're gonna say. For int i equals zero. i lesser than, let's just say, a hundred thousand. i++. dishesOut. And increment. Okay. So, now, each cook will send out a hundred thousand dishes.

A busy night. The join in here, means wait here until that cook is done. A hundred thousand plus a hundred thousand. Normally, one would say two hundred thousand. Everyone will say that. But run it. And that's not two hundred thousand. Run it again. Yeah, that's different. And the wrong number. It can't even be wrong consistently. Remember the Race Condition Risotto? Portion size may vary. Now you know why. Here's the crime.

dishesOut++ looks like one step. It's actually three. Read the board. Add one. Write it back. Cook A reads the board. 41. Cook B reads it at the same moment. Also, 41. Cook A adds one. Writes 42. Cook B adds one. Writes 42. Two cooks each sent a dish. The board went up by one. A whole dish just evaporated. Do that dozens of times and you lose a big chunk of your count. And since it depends on exactly when the two cooks bump into each other, you get a different answer every run.

This is called a race condition. The threads are racing and the result depends on who wins. The fix? Just one marker. Only the cook holding the marker is allowed to write on the board. Everyone else waits their turn. In C#, that's called lock. And can be showcased by doing this. Stop the program and say object marker is equal to, let's say new. Down here in the SendOutDishes dishes method. Let's just say lock (marker).

Only if this cook that's running in the thread has this marker, can he increment the dishesOut. I've literally named it marker. It's just an object. It doesn't do anything except exist. This means grab the marker. If someone else has it, you're gonna wait. Then read, add, write. All three steps with nobody interrupting. Then put the marker down. Let's now run it. There's once, 200,000. Twice, 400,000. Three times, 600,000.

It increments by 200,000 every single time. It's a little slower now because cooks spend time waiting for the marker and safety costs a tiny bit. But it's worth it. Now, back to our kitchen. Let's say I spam the button to chop vegetables multiple times. As you can see it's behaving a bit erratically. The chopping progress is jumping forward and backwards all the time. Every C press hired a new prep cook. I've literally told you.

Past Leo told you. Three prep cooks are writing their own number on one progress board. It's a dishes bug again. Wearing an apron. We need two things. Only one order at a time. And a way for the head chef to know when the food's ready. Meet the task. So, new Thread is like hiring a cook for one dish, then firing him. Hiring takes time. Real programs don't do that every time. They use a thread pool. Like crew of cooks already on shift.

Waiting at their stations for the next ticket. So, first, the prep cook stops touching our veggies list completely. He just returns the finished plate. Heyo, editor Leo here. Or as Past Leo called me, Future Leo. And yes, I do regret it. While editing, I found a hole that Past Leo jumped straight to await without ever showing you what it is. So, let me catch you up quickly before he notices. First off, ChopVegetables now returns as a list.

Instead of putting the plate on the pass himself, the prep cook just hands it back. Task.Run means put this ticket on the rail. Whoever's free grabs it. You get back a task. Your copy of the ticket. Is the order up yet? When it is, the plate comes with it. Now, everyone online says use async or await. Or the combination of the two. So, Past Leo wrote this. await. Wait until the order's up, then take the plate. Sounds perfect.

Except, when he ran it, the whole kitchen froze. Again. Spinner stuck. Clock stuck. Progress sitting at zero. And then, five seconds later, boom. Straight to 100%. Like nothing happened. And the head chef isn't even chopping this time. He's just standing at the pass. Staring at one ticket. Alright, I'm done. Back to you Past Leo. So, this one is a bit of a malicious compliance. await means wait. I told it to wait. And it waited.

Here's the thing nobody tells beginners. Task.Run is the do it in the background part. That part worked. A cook from the crew really is chopping on another thread. But, await says pause this code right here. Until the order's up. And, this code is my So, the head chef plants himself at the pass, stares at that one ticket. And ignores every other order. No input. No screen updates. Frozen spinner. await is brilliant. When the next step genuinely needs that plate before anything else can happen.

Like, download the file. Then open it. But, at the pass. The pass must never stop. So, the head chef doesn't stare at the ticket. He clips it up. And, glances at it every loop. For that, let's create, up here, a new variable called, like so. So, Task<List<string>>. It's a nullable. With the name, veggieTicket. And, say it's a null. Now, down here. Instead of Task.Run ChopVegetables. Let's just say. veggieTicket.

Is gonna equal to Task.Run(ChopVegetables). So, instead of awaiting. It's gonna say. This ticket. Belongs to this task. And, down here. Before we start the veggies. I'll just say. If veggieTicket. Is not equal null. And, veggieTicket. IsCompleted. So, once it's done all the steps it needs. We're gonna say. Veggies. Equals. veggieTicket. Result. And, our veggieTicket. Is going back to null. veggieTicket. Is our copy of the ticket itself.

So, when it's null. It means. No veggie order. Is in progress. So, mashing C only ever starts with one order. Now. The button masher has been defeated. Then. Every loop. It's gonna check. Is the order up. Is it completed. If yes. The result. Will take the plate. And, we clear the ticket. So, we can order again. And, notice who puts the veggies out now. Not the cook. It's the head chef. At the pass. The cooks cook. The head chef.

Sends it out. Remember that. It's about to matter. So, if we debug this. And, spam C. It's spinning. It's ticking. Chopping. It's mash proof. The head chef is back at the pass. But, a kitchen with one order. Is quite a sad kitchen. Isn't it? The whole point of a crew. Is that. They can work on different things at once. And, that spaghetti code carbonara. Isn't going to sauce itself. So, let's create a new method. We're gonna add.

The dash. Also. Let's keep progress of it. Like with chopping progress. And, say. sauceProgress. Equals zero. Down here. Same logic. sauceProgress. Equals. I plus one. And, then. It's gonna return the sauce. Simple. Same shape as the veggies. It's its own plate. Its own progress. A bit faster. Actually. Also. Let's. Change this. From 50 to 30. It's gonna make it a bit faster. So. It's the same shape as veggies. It's its own plate.

Its own progress. And, it's a bit faster. And, then. The same moves as before. One more. A ticket. A key. And, a check. So. In here. Let's duplicate this. And, say. sauceTicket. The veggies. Let's duplicate into sauces. And, say. Let's add. Okay. We already added the sauce progress. So. We already have that part. Down here. Let's just say. You're gonna wait for. The S. To be pressed. And, in here. It's gonna assign to the sauceTicket.

Task.Run. Prepare. Sauce. Just like that. We can copy. This. If. Statement. Say. If. sauceTicket. Is not equal to null. And. IsCompleted. We're gonna assign to sauce it. The. sauceTicket. Dot. Result. And, then. sauceTicket. Is gonna be null. So. Exact. Same logic. It's copy paste. It could keep all your tickets in the list. A different video. But. Yeah. Let's go back to the topic at hand. So. Now if I go here. Oh. Wait.

We forgot one step. We are not writing the progress. So. Simmering. Progress. Sauce. Give me a second. Like so. Let's run the program. And as you can see. It stays at 0% for now. If I press C. Press S. The progress is incrementing at the same time. There goes the veggies. Okay. But. Our sauce is doing nothing. And why is that? Because I didn't assign anything for it. So. Let's add a new foreach. That's gonna be for the sauce.

Let's also add this Console .WriteLine. And instead of this. Let's add this. Now. If I go back to Chez Léo. Let's press C and S. It's doing its thing. And there it goes. So. Let's also add a new line break here. Because. It's putting the veggies and the sauces. And the dishes out like. All jumbled up. So. Yeah. Like this. It should now look a bit cleaner. So. C and S. As you can see. The progress increments. Independent of each other.

It starts writing. First sauce. And then the veggies. Because the sauce is complete first. I can hear you guys now saying. Why do you care so much. On who sends the plate out. Let's do a quick experiment. Now. In here. I'm gonna add a new key. So. Let's just say. Key equals. Console. Key. Let's just say. Since it's auto-filled that. And. Let's just say. Console. WriteLine. And. Let me. Quickly check my notes. On this one.

Order. Handed off. By thread. And. Let's just say. Environment. Dot. Current. Managed. Thread. Id. Let's just say. await. Task. Run. Like so. Thread. Sleep. 500. And. Let's copy. This one. Here. Uh. Think I'm missing a parenthesis. Yep. And. Plate. Brought back by thread. Like so. So. Now if I run this. And press A. Uh. Let's just. Stop. Everything else. Down here. So. All of these. Console. Dot. And everything like that.

I'll just. Clean them up. If I now. Debug this. Press A. Order. Handed off by thread 2. Plate. Brought back by thread 7. As I said. Every thread. Has an ID number. The head chef. Handed off the order. On thread 2. And the plate. Came back on thread 7. Some random cook. From the crew. Brought it back. In a console app. Nobody cares. About who carries the plate. But in a program. With a window. Everybody cares. Remember.

The first struggle we had. The head chef. Walking through a list. While someone else was changing it. The window. Is the dining room. Buttons. Text boxes. Everything on screen. Belongs to the head chef. And. He's walking through it. Constantly. If a random cook. Barges in. And starts. Rearranging tables. It's the same crash. Or even worse. So. They enforce. A golden rule. In windows forms. Touching a control. From another thread.

Gets you. Cross-thread. Operation. Not valid. In. WPF. The calling thread. Cannot. Access this object. Because a different thread. Owns it. Which is the most. Passive aggressive. Error message. Ever written. Game engines. Like Godot and Unity. Same rule. And. Every one of them. Gives you a way. To say. Please do this. On the main thread. It's a different name. In every framework. But it's the same idea. The cooks cook.

The head chef. Runs the pass. Every time. So. We started with the kitchen. That died. Every time. Someone picked up a knife. Now. It chops and simmers. At the same time. Keeps ticking through all of it. And you can't break it. By mashing keys. And. The three crimes. So you don't commit them. One. Sharing a list between threads. Collection was modified. Exception. You give each cook. His own plate. And. He puts it on the pass.

When it's done. Two. Two threads. Doing dishesOut ++. The dishes vanish. You lock it. Three. await inside your main loop. The loop waits too obviously. You keep the ticket. And check it every loop. If threading finally clicked. Do hit the like and subscribe button. It helps keep the lab open. Go find a thing that turns your head chef into a prep cook. And hire some help. Class dismissed.

Watch on YouTube ← All Videos