 I hate to ask you to do this, but can you take this once you start the video? It's just going. Oh, it is? Yeah. Okay, great. Let's get going. Okay, so we have a lot to do today. We're going to talk about synchronization primitives. How many people are glad it's Friday? All right. The people who are not here are probably even happier it's Friday. You might be happier it's Friday if you weren't here. All right, so today we're going to try to get through locks. So we'll start off by talking about critical sections, which we sort of got into last time. And then we'll talk about two different types of locking primitive and when to use them. This is information that hopefully will be starting to line up with the work you're doing on assignment one. How many people have started assignment one? All right, good. If your hand isn't up, you have a week. All right, that's enough time. It's not going to continue to be enough time if you don't do something proactive about it. All right, so just a couple announcements. Remember next Wednesday and Friday, I won't be here. Guru will be here. Guru will be screening classic lecture videos from last year. And that's what we'll do in class. So you're also welcome to watch those online. It doesn't matter. But if you want to come, bring popcorn, we'll show them in here during the hour. Also, recitation on Friday will not happen. I'm sorry for those of you that were looking forward to getting up and getting here at 8 a.m. for the Friday morning recitation next week. Jing Hao will be with me in Santa Fe. So we will not do that. However, Tuesday's recitation like last week, like this week, will be taped. And we will post that video online. So if you normally go to recitation on Friday or if you normally don't go to recitation, the video will be available. These are all on the calendar. So trust the calendar. The calendar's always right. Assignment one is due Friday the 13th. Next Friday. It turns out, actually, I realized that we have two assignments in this class that are both due on Friday the 13th. This is the first one. Assignment two is due exactly a month later. And again, thanks to the collaboration between February and March, February having a number of days. It is divisible by seven. I don't know how many years this happens, where there are two Friday to 13th and back-to-back months. Maybe somebody can look that up for me. But anyway, that's what's going to happen. Finally, I don't know how many of you guys were able to make the event yesterday, the Scientista event. I want to...it's Kendra here. Kendra, Natasha, Gila. Apparently, the event was so great they decided not to come to class today. But some of your classmates were involved in organizing this in one of the ninjas. If you guys want to join the mailing list, this is the URL. This is really, I think, something that's really important for our entire field. Here she is. We're just talking about you. Yeah. So let's all give Gila a round of applause for the event yesterday. It was awesome. All right. And there's the URL if you want to sign up for the mailing list. I hope that's right. I just typed it up. It looks right to me. All right. Yeah, there we go. There's the assignment. There's the Friday to the 13th, man. Finish your locks the first time, or they will be back. You just don't want to mess with the Friday to the 13th assignments. Either one of them. Maybe this is before your time. I never saw these movies either, but anyway. OK, so let's talk about what we started to talk about last time. We'll just do a little bit of review. So the thing to keep in mind with synchronization, I know we've shifted things around this here, is that you have to stop making assumptions about the order in which your threads run. So what are the things that you need to keep in mind whenever you are working in a multi-threaded environment? What are some of the assumptions that I have to make? Or the assumptions that I can no longer make? Who remembers one of them? Unless you do something about it, what will threads do? What can threads do? Yeah. They can run. So essentially they can run in any order, right? They can run in any order. What else can they do that you may not want them to do? They can be stopped at any point in time. And restart it at any point in time later, right? And they may remain stopped for an arbitrary period of time. This is how we multiplex the CPU. And again, we'll come back to that in a week. But when we start thinking about synchronization, this is what we have to remember, OK? And again, normally these are things that we want to happen. But when it starts to come to synchronization, what we need are some tools that allow us to impose some control over what would normally be sort of an arbitrary collection of thread stopping and starting at random moments, right? There's times in which we need to preserve certain properties. And we'll start talking about that today. All right. A race condition is when the output of a program is unexpectedly dependent on time. So you run the same piece of code twice. And the first time everything runs perfectly. And the second time something bad happens. Now what people have found in this course I just want to warn you is that somehow, and I don't know how we did this, but the auto grader is extremely good at finding that time that your code that has a race condition doesn't work. It may work a hundred times on your local machine. And once you upload it for grading, that'll be the time it doesn't work. I promise you. And again, I don't know how it does it. It's sort of spooky. I don't even want to look into it. I feel like the auto grader has developed some sort of intelligence of its own at this point. And so I'm not really that curious, but it's good. Because we want to find that one time. As a programmer, you want to find that one time. Maybe it doesn't happen to you. But when you guys get out into the real world, it'll happen to a customer. And that customer will be mad. And again, remember that the race condition depends on what we expect it to happen. So we want a certain thing to be true. And when we start talking, so today we're going to go through some of these primitives. On Monday we will use some of the primitives we discussed today to try to solve some real synchronization problems. You guys have a few more that you're going to do as part of assignment one. And on Monday we'll go through some in class. But a lot of times when we start approaching one of these problems, the first thing we do is we decide what do we want to happen? What do we want to be true? What are the invariants that we want to preserve? And when we talked about the problem, we talked about the bank example last time. And there we knew what the answer should be. And it's just a question of ensuring that no matter how the threads are run, the correct answer is going to be the one that they're going to arrive at. And we'll go back to that example today. And fix it. So concurrency and atomicity. Concurrency is the illusion that there's a lot more going on on your system than you think there is. What is atomicity? Oh, does this still work? Sweet. Todd. Where is Todd? He got nothing. Let's try deep. Now technically someone could just be sitting here who's actually deep and not saying anything. I can't look at 100 people in that span of time. We're just going to let him off the hook. Shashank. Yeah. So atomicity is the illusion that a bunch of things that the machine actually has to do separately happen all at once. It's one big indivisible blob. And when we go back to our bank example, that's what we wanted to happen. We wanted that entire series of instructions to look atomic. Now there was a lot going on and there was a bunch of things that the machine has to do. And so we can't rely on the machine to ensure atomicity for us. We talked about things that might look atomic to you in C are not actually atomic. Just initializing a variable might require several instructions. So you can't assume that a single line of C code is even atomic. There might be other things that the machine needs to do to actually implement that. So we need ways of building atomicity despite the fact that what the machine is giving us are these very, very low level instructions. The machine has no instruction increase the balance in this account by this amount. That would be a pretty, pretty huge architecture if you needed a special hardware instruction for everything you needed to do atomic. So we need a better way of doing that. That's what we'll get to. All right. What is a critical section? I think we talked about this last time. Of course, that was two days ago. Yeah. Yeah, so critical, you're almost there. I'm going to give you points for that. A critical section is a portion of the code that we only allow one thread to be executing at any given point in time. And you can see there's a natural connection between atomicity and a critical section. Frequently the way we achieve atomicity is we put the things we want to be atomic inside a critical section. That way they look atomic to threads outside. Right. Okay. Right. I just said awesome. Okay. So when we went through this example, we identified the private state, which is this local variable that represents how much money I have. We represented, we figured out what the shared state was. And we identified that lines two to four, maybe five, depending on how much you want to irritate your customers, are inside the critical section. And this was what we started. Okay. So this is new stuff. I forgot to have a question. So the amount of questions with the stuff that we just went through before we plunge onward. I mean, the illusion of, yeah, I think it could be both. I mean, the illusion of concurrency is something that you guys experience every day. Because you think that your computer is simultaneously doing a bunch of things that it's not. I think the illusion of atomicity is probably something that we experience less when it happens and more when it does not happen. So for example, I try to add something to my shopping cart online and it adds it three times or something like that, something dumb. So you see this more when it breaks. But concurrency you see all the time. That's just, that's how you've always experienced computers. And when we come back to see if you have multiplexing, we'll talk about some changes we can make to your computer that would cause you to stop experiencing concurrency. So for, you know, if your computer always ran the same thread for like a second at a time, then your computer would be very interesting. It'd be like a second's worth of music, a second's worth of mouse movement, a second's worth of music. It's probably not how you want your computer to work. Okay. So requirements for a critical section. These are the design requirements for implementing various types of tools that allow us to create critical sections. So the first one is simple. There's only one thread inside the critical section at a time. We call this mutual exclusion. If I'm inside the critical section, I am excluding all other threads that might be trying to enter. Right? Progress. Eventually, once I'm inside the critical section, I need to be able to get out. And what that's going to allow is all of the other threads that might be waiting to enter should eventually be able to get inside and do what they need to do. Right? And finally, performance. And this is less of a design requirement for critical sections and more of a suggestion about how to use them. When we use critical sections, we normally want to keep those parts of the code as small as possible. Because what's going to happen is critical... Like we talked about, animosity and concurrency are sort of at war with each other, right? Concurrency is the machine trying to make efficient use of resources by stopping and starting things when it thinks they need to be stopped and started. The critical section is me telling the machine, you know what? The fact that you stopped a thread inside the critical section means there's several other threads that can't make any progress. Right? So these two things end up conflicting with each other. And the way that we try to preserve as much performance as possible while still ensuring correctness is making sure that the critical sections are short. Right? So in general, if you can set things up so you do a lot of stuff outside the critical section and only modify the shared state inside, that's an ideal way to do things. You guys have to experience this, right? There are some really nasty ways to get Assignment 3 to work that involve like locking around the entire virtual memory system. Right? That works. It also really stinks performance-wise. Sometimes, somehow I manage to have a filter when I'm in class and not say bad words. I don't know how that works. I hope that holds up this year, but it's been working well. All right. So how are we going to implement a critical section? There's two ways to do this. The first way is don't stop when you're inside the critical section. Once you get in there, don't stop running. Right? Again, if I stop and I don't do the other thing, then it's possible somebody else will get in. Right? So one way is as soon as I'm inside the critical section, ensure that the system will not stop me from running. The second way is don't allow other threads to enter the critical section while one's inside. If I pursue the don't enter path, I can allow myself to be stopped because other threads won't be able to get in. But if I don't have a mechanism for stopping threads from entering the critical section, I sure better not stop inside of it. Right? Now, and this is sort of, I don't know why I talk about uniprocessors anymore because even your smartphone now has like four cores. Anyway, but of historical interest for those of you that are curious about the past, at one point in time there were these things called uniprocessor machines that only had one core. And on those machines, there was this nice trick that you could do to create a critical section, which is you could simply prevent the operating system scheduler from running. If you did that, you were fine. Right? Because there was only one thing that was happening on the machine, and if you're inside the critical section and you're the thing that's happening and you prevent the operating system from stopping you, you're done. Right? On multi-core machines, this is broken. Right? Because there are fundamentally multiple things that are happening. So you might think that this is going to work. But in reality, if the operating... So I'm inside the critical section and I'm thinking, hey, I stopped the operating system scheduler from scheduling other threads, but it turns out that the operating system scheduler scheduled a thread before you stopped it that's about to get inside your critical section. Right? So this doesn't work. Right? More generally, what we really want to do is pursue the don't enter strategy. We want a way to prevent threads from getting inside of the critical section while one of... While it's in use. Right? Once a thread enters, we want to make sure that nobody else can enter as well. This is something that's much more robust and much more effective on multi-core machines. So particularly for multi-core machines, the way, you know, and this is true, we're going to find this over and over and over again. The operating system to do most of what it does relies on a really synergistic relationship with the underlying hardware. Right? There's a lot that we can do in software, but frequently it boils down to having this one thing in hardware that we need to work to help us do what we need to do. And with synchronization, the thing we rely on are these special hardware instructions that are guaranteed to be atomic or have certain properties even across multiple cores. Right? And this is something that really I need to rely on hardware to do. So let me give you a couple of examples of these. Right? The first is what's called an atomic test and set. A test and set writes a memory location and returns its old value. Right? And the idea is that the underlying hardware will ensure that this instruction is atomic with respect to other cores that are accessing the same memory location. So you can see, even if I write this out in C code, this is C pseudocode for the test and set instruction. This is not something, this is not how it works, right? Because remember, hardware has to provide this as a single atomic instruction. Right? And not only does hardware have to provide it as an atomic instruction, it has to be atomic across multiple cores that might be trying to test and set the same value. Right? So for example, if I have a memory value that has the, if I have a memory address that has the value zero and I have four cores that try to do a test and set where they change the value to one, how many cores should receive the value zero? Yeah? One. One. Only one. That's the critical part. If I did this, if I did this normally it's possible that it would actually take two hardware instructions and so it's possible that one core, two core, four cores could get the value zero. But the test and set instruction ensures that one core gets zero, every other core gets one. Right? There's something else. So this is potentially more useful. Compare and swap. So compare and swap compares the address in memory to a value that's passed in. If they are the same, then it sets a new value. Right? So you can think of it like this. I pass in a target memory address, a compare and a new value I want to set, and it succeeds only if the value is the same that the one I pass in. So same example. I've got, let's say I have four cores, core one, two, three, and four, and each core tries to compare and swap a memory address that starts off with the value zero with their core ID. So core one does one, core two does two, core three does three. Okay? What do you need to know to tell me what the value of that variable is? Now imagine they do this at the same time. As simultaneously as I can get this to happen. So how many cores receive the value zero after they do the compare and swap? All cores are going to send in zero. Right? Because that's what they're comparing against. Right? How many times will this instruction succeed? Four times? Once. Right? And what will the value of the variable be after this finish is executed? It could be one, two, three, or four, but the same core that gets back zero is going to be the core that sets the value to whatever they sent in. So if, for example, if core two gets zero back from the compare and swap, it knows that the value is two. Right? Because it won. It won the compare and swap. Right? Okay. Here we go. Link load and store conditional. So why are we talking about link load and store conditional? Does anyone know? Yeah. Load link. Sorry. Yeah. Well, I don't know about the MIPS architecture. The pseudo MIPS architecture that you guys use to do your OS 161 assignments implements this in hardware. Okay? And this is a little bit trickier. This is actually a combination of two instructions. Right? The first instruction essentially tries to, it's almost like it's trying to lock the underlying memory and then the second instruction will store a value only if the value hasn't been modified. Right? So think about it this way. The first instruction I tell the hardware, I'm about to change this memory address. But I'm going to tell you what I'm going to change it to in a minute. Right? Then when I do the store conditional, what I tell the hardware is, if that memory address has not been modified, since I told you I was going to change it just a second ago, then store this address. So think about it. The memory hardware, right, actually has to keep track of this for you. It has to know, oh, okay, there was this load link instruction that was executed a few cycles ago, and then this other core wrote to the address, so I need to tell the core that's trying to do the store conditional that this isn't okay. Right? So here is a little bit of MIPS. How many people have seen this code before? All right, how many? It's like the tiny, tiny subset. This is in your OS 161 source treat. This isn't like the machine dependent code inside the MIPS directory. Right? This is how when you guys try to use spin locks, this is the actual hardware instruction that is eventually being generated by your compiler. Right? So like I said, high-level primitives that you guys are using, like spin locks and locks and other things that you guys will build for assignment one, all come down to needing this particular set of very special machine instructions. Right? Load link, store conditional. Any questions about these? I am not going to explain GCC ASM syntax because I barely understand it myself. Right? But essentially this is a way of telling the compiler to actually output assembly code. Right? A very specific block of assembly. Right? There's, you know, this is a machine-specific feature and so there's no higher-level primitive that MIPS, that C supports that maps down to this. Right? Because not every, even not every MIPS machine has this set of instructions. I'm not even sure, so you have to keep in mind, MIPS R3000 is a really old architecture. I doubt that there were, I don't know, I shouldn't say this on camera, I doubt that there were MIPS R3000 machines that had more than one core and so I'm pretty sure that David just implemented this himself in the simulator and I don't think this is historically accurate in any way. Right? But we wanted to give you guys a multi-core simulator eventually and so we needed this sort of support. Right? So David shows he wanted to do load link, store conditional. It's kind of a cool set of instructions. All right. Any question about atomic instructions? Yeah. Yeah, yeah. So sometimes this is referred to as a test, test and set. Right? Don't remember why that is. I'll have to look it up. It's a test, test and set is a variant of test and set. Right? And I'm sure someone could find out what it means on Wikipedia and I'm sorry I can't explain it right now. Students one, Jeff's two. Okay. Any other questions? How about a question I can actually answer? No one's going to ask me. Okay. All right. So in order to allow us to build up these higher level synchronization primitives, remember the hardware has to give us this help. Right? The hardware has to provide these instructions so that we can use them. Right? Or there's, or you know, I can build, there's some hardware support for synchronization and I can build similar instructions in soft. Right? But there's always something at the end of the day where I need some help from the underlying hardware. Okay? We'll go through the aside. This is kind of cool though that some of these synchronization primitives don't scale very well. If you want to solve this problem, you can come work in my research group. There's still people working on this. And, and if you guys, so let me ask you a question. I mean, why don't, why don't we have like, you know, 1,000 core processors? I mean, I've got 64, right? I mean, five years from now are you going to have a 512 core processor? How many people think they will? Aren't you guys hopeful about the future? You guys were millennials. You guys supposed to be like, of course, yeah. You know, maybe you think you'll have more? I don't know. Yeah, there start to be some bottlenecks here, right? Especially with memory, right? You know, memory, particularly once you get to big core count machines, the model of memory that a lot of systems rely on is, is starts to melt down, right? So probably not going to, you probably will see some kind of machine with that many cores, but it's going to be different than the kind of machines that you're used to. It's not going to give you some of the same really nice guarantees that you're, you're normally used to. Okay. So let's go back to our bank example and let's figure out how to fix this synchronization bug. So we know we have a problem because we were able to show that there were several cases where the interleaving of the threads caused me to get different answers. What is that called? It's called a race condition. So my code before had a race condition, right? Here was the code before. And now who has a suggestion about what I should do? So I'm going to try to use a test and set to fix this problem. Yeah. Yeah. So I essentially want to use a test and set to establish a critical section around the part of the code that I need to be atomic. All right? So here we go. I create my variable that the test and set is going to modify and then I'm done, right? How many people think this works? What's wrong with it? I think this looks awesome. First, it's even like color coded a little bit too. I have no idea how the syntax highlighter works. It's clearly kind of strange. What's wrong with this? So again, I set the test and set at the top from 0 to 1 and then at the bottom I clear it. Why won't this work? Will this work? There's nothing to do with scope. Yeah. What am I doing with the output from the test and set? Remember, the test and set is going to test at the variables a certain thing and return the old value, right? But if I don't use the old value, does this code change the operation of this function at all? No, right? I mean, I clearly need to do something if there's another thread that's already inside the critical section. Okay? Yeah, so how do I tell? I need to consume the output of the test and set. Okay, so let's try this again. So I put in my if block at the top. So what does it mean that the test and set is 1? What does that indicate? That there's another thread inside the critical section, right? I initialize the test and set to 0 when I started and so the only way it's 1 is if somebody is between this called the test and set and this called the test and set, which is where I've established my critical section. So I know there's another thread inside of it if this returns 1. But then, what do I do? Wait, like that. Well, there's a couple of problems, right? So I mean, I need to wait until the value is what? 0. So let's try this again. Now, so now I have a while loop here and here's what I'm going to do. When I get to the top of the loop, I'm going to test the test and set. If the test and set is 1, I'm going to test it again and I'm going to continue to do this until what's going to break me out of this loop? When it finally turns to 0. So does this work? How many people think this works? Yeah, this works. How many people think this is a good idea? So what's the problem with this? So let's say there's a thread inside the critical section and another thread, maybe it's running on the same core because the thread that was inside the critical section was stopped. Threads can be stopped and started at any time or maybe that thread was running on another core. It hits the top of that critical section and it just sits there waiting. But it's waiting by executing instructions. So we call this busy wait. So here's what would happen. Again, go back to the example. This guy is starting to deposit some money. He hits the top of the test and set he's fine. He goes right through because the test and set is 0. This guy gets there, he hits the test and set and he hits the test and set and he hits the test and set and he just keeps doing this. So the B student is actively waiting. Again, we refer to this as busy waiting. It's waiting that is consuming cycles on the process. And yeah, this is not good, especially when you guys are trying to earn your grades at the class. Eventually someone has to win. So the problems with this approach are the one we've identified. We don't want to busy waiting. And it turns out busy waiting on these... So it's worse than you think because these hardware instructions that are used to implement critical sections like the atomic test and set or load, load and... leak, load and store conditional are actually frequently pretty heavyweight instructions for the hardware to execute. They require coordination potentially between the cores. So these instructions are not only... It's not only bad to just sit there doing nothing with the processor, making no progress, accomplishing very little, but these instructions are also frequently not ones you want to execute overnight. And on a multi-core system, this could be bad. And we'll talk about trade-offs with busy and sleep waiting in a minute. But on a single-core system, this is really bad. Because on a single-core system, keep in mind, while I'm using the processor, nothing else is going to happen. So if thread... If the B-student is sitting there whacking on the door, all he's doing is consuming CPU cycles that the other student needs to make forward progress. So this is really, really bad on a single-core system. But again, who cares? We don't have those anymore, so it doesn't matter. So what we've implemented today is a lock. A lock is a synchronization primitive that is primarily used to create and protect critical sections of code. There are other uses for locks, but this is the one that we're going to start with. So when I acquire a critical section, I acquire the lock. When I leave the critical section, I drop the lock. And what we've built today is a spin lock. It's a lock that guards a critical section, and spin describes the process of acquiring. Typically, when you talk about locks and see lock implementations, a lock without a prefix refers to a lock that does not typically spin. A spin lock is used to describe a lock that will actually busy wait for the resource. And there are reasons to use spin locks. We will get to those in a second. But again, if you hear about something that's a lock, you can probably rely on the fact that that's actually going to stop the thread from running while it's waiting, and we'll show how that happens in a second. It's unlikely that you will need a spin lock to solve a synchronization problem. There are places where you can use spin locks, and we'll talk about the trade-off between spinning and sleeping in a minute. They're frequently used to build higher-level synchronization programs. So if you guys have examined the semaphore implementation, you'll see that the semaphore implementation uses a spin lock to safely bridge to the wait channel. So the wait channel bridging is designed to ensure that the condition that I'm about to go to sleep on doesn't change between the time I start to wait and the time that I actually arrive at the wait queue. If that happened, I might end up waiting forever, which would not be good. All right, so let's go back to this, and voila, here's what we have. We have a lock. I call lock a choir at the top. I call lock release at the bottom, right? And this is a cleaner way of doing this lock acquire. The semantics of lock acquire are that when it returns, I hold the lock. What it does until it returns is its problem, right? But when it returns, I hold the lock, so I don't need to introduce any extra conditionals into my code. Any questions about this before we go on? Okay, so what happens if we call... So now that we sort of abstracted away the details of the lock, what happens if we call lock acquire when there's somebody else inside the critical section, aka somebody else has acquired and is holding the lock? What does it do? It has to wait until the other thread calls lock release, right? And then, so if I call lock acquire and another thread calls lock release, this would be a great short answer exam question that I'm giving away right now, right? I could have put this one away in my back pocket and used it on the midterm, so it will do the true, false in class anyway. If I call lock acquire and another thread calls lock release, then I acquire the lock, true or false? True or false questions are too easy, right? False, why not? What else could have happened? Could be somebody else waiting, right? When a thread holding a lock calls lock release, it means that some thread that has tried to call lock acquire, if there are threads waiting, one of them will make forward progress. If there's not, then the next thread that calls lock acquire Okay, so how do we wait for the lock to become available, right? The first thing that we presented is this active waiting strategy, right? So I literally repeat some action until the lock becomes available. The action I was repeating was this underlined atomic instruction provided by Harper, okay? The other approach is passive waiting or sleeping, okay? And this requires more help, right? This requires the operating system or, you know, the thread library or whatever to provide me with a little bit more help doing this, right? So essentially, the way you can think about passive waiting is when I go to sleep on the lock, when I try to acquire lock and the lock is held, I tell the operating system, I am waiting for X to happen. And when it happens, please let me run again. And the operating system says, fine. Sounds good. Go to sleep. When another thread calls lock release, the operating system looks for all the threads that are waiting on that particular thing to happen, and it wakes them up. All of them, right? Now, it wakes up all of them. One of them is potentially going to be able to acquire the lock again, and then we repeat this process, right? So again, this requires a little bit more support. Now, there are cases when spin locks are actually an appropriate primitive to use, right? Yeah? Yeah, sure. Ah, okay. So everyone, someone usually asks this on piazza, but I'm glad someone asked it in class. So let's say I have 10 threads that are waiting on a particular lock to be released, and the lock is released. How many threads should I wake up? First one in the queue? That's interesting. So when I took this class, I thought I was all clever, and that's what I did with my locks. I was like, these locks are dumb. I'm going to build a better one, right? And so I built this extra function. At the time, there was no way to just wake up one thing that was waiting on a wait channel. Now, David has that feature. I'm sure he copied my code for that. Anyway, so the point is, I thought this was a great idea. I'll just wake up one thread, right? So it turns out, and we'll come back to this when we talk about thread scheduling, usually I don't just want to wake up one thread. Because the code, so which thread runs is really not the lock's decision to make, right? Maybe I have 10 threads sleeping on that lock, but one of them is really important, and the other ones aren't. Usually, the lock application has no idea what's important and what's not. So what it does to be safe is it wakes them all up, and then it lets the thread scheduler decide what to do. And again, we'll come back to scheduling in a week or two when we talk about policies that we applied to how to abstract the CP, right? But again, it seems counterintuitive. It seems it would be like a lot better to just wake up one. But the problem is I'm trying to separate things out. I want the scheduler to make decisions about what to run, and the lock implementation just implements a lock, right? So when I wake up things from a lock, I normally wake up everybody, right? And I just say, hey, scheduler, you figure out what to do next, yeah. Yeah, so that's a great question. What's your space so it end up, you'd be right, because as soon as I wake up a thread, the operating system can schedule it, right? What's happening here is I am the operating system, right? So I'm thinking about it from that perspective. And while I'm waking up the thread, I'm running, right? So nothing else can really run, right? So essentially, I can mark, it's a great question. I can mark a bunch of threads as being ready to run, okay? They end up in a queue of threads that's then chosen by the scheduler the next time there's a scheduling decision to make, right? Does that make sense? Great question. Any other questions? Okay. And again, remember, only on a multi-core system do I ever want to spin, because on a single-core system I'm just blocking another thread that is going to change the thing that I'm waiting for, okay? But the main reason to spin in a critical section is because if a critical section is very short, okay? And what we're doing here when we decide whether to spin or sleep is we're balancing two overheads, okay? The first overhead is the overhead of spinning, because again, when I'm spinning, I'm wasting time. I'm wasting CPU cycles that somebody else could use, okay? So this is overhead to repeatedly trying to see if something has happened that I'm waiting for. On the other hand, and again, unfortunately, this is a consequence of putting this material before threads rather than after, there is an overhead to stop a thread from running and start another thread. We call that process a context switch and we will talk about it next week or the week after. So there's these two overheads here. This is overhead to stopping one thread and starting another, which is what I'm going to do if I sleep. And there's the overhead of repeatedly doing the same thing over and over again and hoping that something different is going to happen, and that's the overhead if I spin, right? So here's an example if the critical section is short, but I choose to sleep, right? So thread one acquired the lock, but the critical section was very small, all right? Sorry. Every year, new bugs on the slides. Thread two, the red thread, we'll just call them red and green, right? Red thread had to try to acquire the lock while thread one was in the critical section and it shows to go to sleep, okay? And so there's this whole process that the processor has to perform in order to put it to sleep and then later to wake it up again, right? And by the time it wakes up, thread one is long gone. Sorry, the green thread is long gone. It's actually the critical section a long time ago. So here it would have been a better idea to spin, right? Because I would have acquired the lock right here, right? And the amount of time I wasted spinning would actually be smaller the amount of time it took the system to put me to sleep and wake me up again later, okay? If the critical section is long and I spin, then I might end up wasting more time than the context which would have taken. So here's an example where the red thread acquires the lock and actually this lock needs to be held for quite a bit of time. Maybe the thread actually has to be able to sleep with the lock. It's going to do some I-O. It's like waiting for tomorrow to happen or something. Who knows, right? And in the meantime, thread two, sorry, the red thread is constantly just sitting there, are you done yet? Are you done yet? Are you done yet? Right? I mean, and that's just wasting lots of CPU cycles. Okay? Do you guys understand this trade-off? It's pretty cool. All right. So sleeping, if you guys have looked at weight channels, you guys have seen this happen. You can sort of map from weight channels to thread sleep. When I sleep, again, I sleep on a key. I tell the operating system, wake me up when this thing happens. And when I wake, I pass a key and I allow the operating system to wake up all the other threads that are asleep. How many people have looked at weight channels already? Did Jing Hao do that in recitation? Okay. Well, you guys will see them soon if you have. And weight channels implement this piece of function now. Essentially, right? It's, you know, weight, W Chan sleep. Is that the name of it? This is what it was called when I took the class for real. I'm not kidding. So this is what I remember. W Chan sleep, W Chan wake one, W Chan wake all. I mean, it's essentially this, right? Okay. All right, fine. Okay, so now, now let's talk briefly, because I want to, you know, get through a little bit about communication before we do the problems on Monday, right? Or maybe we'll get stuck here. We'll have to come back. So remember when we started talking about synchronization, there were a couple of things. Well, actually, sorry, let me slow down. Are there any questions about locks? Yeah. What's that? I suspect, yeah, I suspect that spin locks were probably, you know, accompanied the first multi-core systems. But remember, I mean, multi-core systems have existed for a long time, right? Multi-core systems being so ubiquitous that there are a few single-core systems left is much more modern, right? I mean, we've had multi-core systems for, I had, my brother had a roommate when he was in college who had purchased a, and this was, I don't know, he probably went to, started college in 2002, 2001. So his roommate had this really big, expensive computer that his parents had bought him for college. And it actually was a multi-core, it was a multi-processor machine, right? It had like two sockets and everything, man. Those things were awesome. And the thing that was cool was that Dell, well, I shouldn't impugn any particular computer company, right? Some computer company, right? Like Smompac or whatever, I don't know. Had sold him this computer with Windows 98 on it. So, can anyone identify the problem here? Yeah. Yeah, no multi-processor supported good old Windows 98, right? So he had this big expensive computer that was probably three times as expensive as it would have been because it had two processors. And one of them wasn't even being turned on, right? They were being initialized. They were just sitting there, right? Like, maybe it was the backup processor. It was like, I'll fail over to that one later when the first one burns out, right? He was pretty bummed about that. So anyway, he, what's that? That's fair processor. Yeah, exactly. It's like raid with my processors. No, I don't want to do that. Processors don't break, right? So anyway, but yeah, I mean, I think that spin locks probably existed for a long, long time, right? The ubiquity of multi-core is pretty cool. And that's kind of what has driven us to need to use those all the time. Good question. Any other questions about locks? All right. So remember when we talked about synchronization, we talked about correctness? We also talked about communication, allowing threads that are working on a project together, doing some type of, you know, both sort of communicating, working on something to safely communicate. And on Monday, we'll talk about a classic problem where this is the case. I have a bunch of threads. Some of them are putting items into a queue of items that need to be worked on, and other ones are taking items out of that queue and working on them. I don't know why. You feel like, you know, when you do produce or consumer, you always feel like the buffer is the middleman, right? It's like the office space movie, right? I take the specifications from the customers to the engineers, right? Why can the customers do that themselves? Anyway, so we'll talk about a bounded producer-consumer, but any, but you know, producers, items in the buffer, consumers are taking them out of the buffer, right? So there's a case where I have multiple threads, the consumer threads and the producer threads, and they need to communicate with each other about the state of the buffer. Are there items in the buffer? Is the buffer full? What needs to happen, okay? So while locks are designed to protect critical sections, you can sort of think of locks as a signaling mechanism, although I don't want to really push this because it's, but it's so limited, right? Lock release essentially tells all the threads that have called lock acquire, hey, the critical section can now be entered, okay? But there's other types of signals that I might want to deliver, right? Things like the buffer that we're sharing has some data in it that you need to process, or your child has exited, right? This is a big hint for assignment two. And in order to do that, one of the synchronization primitives we've given you guys to work on for assignment one is called condition variables, okay? And a condition variable is a synchronized signaling mechanism, okay? It allows me to do a couple of things. The first thing is wait for some condition to change, okay? Now condition variables have no idea what the condition is. It's up to you to check the condition and use condition variables appropriately. I think people get a little confused about the name. They're like, what's the condition? The condition variable doesn't have any idea, right? It's really just a signaling mechanism. CB Wait says I am waiting for this condition to change, potentially to become true or false or whatever, but I want the outbreak system to tell me when should I reexamine the condition? And then there are two signaling mechanisms that I can use to indicate that the condition has changed. There's usually something called CB Notify which might only wake up one thread, and there's also something called Broadcast which makes up everybody. And in the case of condition variables, this is okay. There are times, and we'll come back to this when we do our example on Monday, where I really have enough information about how the condition has changed that I know that I only want to wake up one thread. And there are other times when I don't have enough information about how the condition has changed that I want to be safe and let everybody who was waiting on a condition know that it has changed. Now, the condition is usually represented as some sort of change to shared state that's controlling how the threads execute. So in the producer-consumer buffer problem, the shared state is how many items are in the buffer. If the buffer is full, the producers have to stop producing. If the buffer is empty, the consumers have to wait before they continue to work. So the condition of the buffer affects how the threads behave and what the threads do also affects the condition. So if the producers put items into a buffer that was empty, now the consumers can work. If the consumers finish, if the consumers take an item out of a buffer that was full, now the producers have more space and they can put another item into the buffer. We'll go through this all in one. And, yeah, so, and condition variables are just a much richer way of expressing information about how the world has changed, right? And I just talked about this, right? Okay. And, yes, we will go through this, like, this example. We'll come back to Hunch, right? So why don't we stop here? We'll pick up condition variables again on Monday and then we'll do a couple of examples. So have a great weekend. Work on assignment one.