 Welcome to Friday. Hope you guys are looking forward to the weekend. Here there's a football game traditionally played at this time of year. Some of you guys might enjoy. Boo? Boo? Boo! Guess Meg was booing, she doesn't like football. OK, so today what we're going to do is talk about, so last class we kind of ended with a mess. So today the goal is to try to figure out how to work the way out of the jam that we created. One thing I'm going to realize right now, oh good, I actually did up this. OK, so pretty much everything I'm going to put online is up, except for a couple of the assignments. Assignment 1 will come out later today. So the TAs are just doing one last check of it. We'll probably post it after class. I think I'm going to set the due date for assignment 0 and assignment 1 as Monday. Does that sound OK? That's enough time? OK, good. Now how about like two weeks from today? Is that more reasonable? There's some people down here who prefer Monday, actually. I'll let the rest of you guys deal with them later. I'll be happy to point out who it was. OK, so yeah, so two weeks from today, I don't know what date that is, the syllabus will be updated with due dates. So yeah, please get to work. Assignment 1, there's actually things to turn in. There's actually a code to write, stuff like that. The assignment, again, the assignment's pretty close to being done. We'll probably put up a little bit more information. We're also still working on the submission tool, but that should probably be done soon. Scott's making good progress on that. And once that he finishes that up, we'll release it to you guys and show you how to use it. It's going to be pretty awesome. We're really excited about it. OK, so let's go back to what we were talking about last time, which is this idea. And this is really kind of assignment 1 prep. So we haven't talked about threads yet. We haven't talked about the thread abstraction or how the operating system actually creates this illusion of concurrency. But you're just going to have to believe me, because what I want to go over today and on Monday is stuff that'll be useful to help you with assignment 1. And that will kind of go back to the normal flow. All right, so when I start to have concurrent programs, I have to change the way I think about how they execute. So if I'm used to thinking about things executing in this synchronous fashion and pretty much executing the way I would read my code, line by line, function by function, once you start to have multiple threads executing those functions, you have to think very carefully about what can happen. Because unless you do something about it, the operating system can stop and start your threads at any time, which can produce an arbitrary ordering. And when a thread stopped, it may remain stopped for an arbitrarily long period of time, while other threads run and other things happen on the system. So and again, the goal here, so when you introduce synchronization into a concurrent system, your goal is to preserve the safety features of the system to the greatest degree possible. You want things to work out as expected. So we're going to try to eliminate that race condition and other types of nondeterministic behavior that we started to talk about last time. However, you also want to preserve as much concurrency as possible, because in general, concurrency is a good thing. We had a world drill malfunction, but they've found the clip. So next time I won't have to use this binder thing. That'd be great. OK, so yeah, in general, you want to preserve as much concurrency as possible, because concurrency is powerful. Concurrency is how your operating system is hiding the fact that there are parts of your system that are really slow. So if every time your operating system or some program on your computer used the disk, if you actually had to wait for that operation to complete, your computer would be very slow. Instead of waiting there, what it does is it has other things that it can do. So concurrent execution is good. This is something we want. So when we start to introduce synchronization, again, the goal is to synchronize the system as little as possible for it to be correct while preserving as much concurrency as possible so it can be fast. All right, OK. So let me just run through this example again quickly. So we had this following code snippet, and then we were talking about what would happen if there were two threads that were accessing this at the same time. And so this is kind of the synchronous case, and this is where everything works out OK. The first thread executes the function pretty much all the way through. The second thread comes and executes the function pretty much all the way through, and I get the result I expect. And this kind of matches your intuition about how this function should work, because when you read through it, you think, OK, this is what I do, this is what I do, and this is kind of the steps that you're going through in your head, thinking about it from a single threaded synchronous perspective. OK. The problem, though, is I can introduce arbitrary interleavings of the execution of these threads that cause things to happen that I don't want to happen. So for example, in this case, I came up $1,000 short, and I can also create a situation where I come out $2,000 short. So this type of behavior is what's generally known as a race condition. So a race condition means that a piece of code will behave differently depending on some feature of the code that you don't want to make it behave differently. I mean, you might think, well, yeah, lots of code behaves differently depending on what I do with it. But a race condition means that every time you open that menu, sometimes it might open, and sometimes it might not open. Every time you run that program, sometimes it might run, sometimes it might not run. It's kind of like what happens when you log into the hub, like sometimes it'll work, sometimes not. That's not what you want to happen. What you want to happen is it works every time, because that's what you would expect. So race conditions create undesirable, unpredictable behavior. And the reason for them is that there's some aspect, and when we talk about synchronization, there's some aspect of concurrency that violates some invariant that the program needs to maintain. So for example, the bank account example needs to maintain that if I make a deposit into the account, the money appears there, and you can easily describe what should happen after a series of deposits. Money shouldn't vanish. That's not good, unless you're a bank, in which case, money can vanish. It's not a problem. And I want to distinguish between the other concepts. One of the concepts we're going to introduce to help us here is this idea of atomicity. So concurrency is the idea that multiple things are happening at once, that the system is dealing. Remember the sort of go distinguish distinction between concurrency and parallelism. The system is dealing with multiple things at the same time, whereas atomicity, in order to achieve concurrency, so there's a nice duality between these two ideas, in order to achieve this illusion of concurrency, I need to be able to start and stop threads at any time. That's how I make good use of system resources, particularly how I make good use of system resources when threads do things that are going to take a long time. A thread might sleep, waiting for you to type a key. A thread might do some I.O. that takes a while. And so that's a great opportunity for me to take that CPU core where the thread was running and how to do something else. OK. On the other hand, atomicity is also an illusion. And it's the illusion that a set of actions that actually take multiple steps, they might involve a lot of different instructions and a lot of different activity by the computer. But it's the illusion that those happen all at once. So if I make a particular function atomic, it means that from the perspective of callers of that function, it looks like that function executes synchronously. So even if a bunch of threads are calling that function at the same time, they're going to proceed through sort of one by one. And to some degree, it's probably obvious that these two concepts are in tension with each other. So atomicity requires that I give threads a little bit of control over their schedule. There may be points in a program where a thread cannot allow another thread to execute a series of instructions because it's in the middle of doing something. But in order to achieve good concurrency, I wanted to be able to start and stop threads at any time. And so this is the tension here. Concurrency is good for performance. Atomicity is good when I want to make certain things safe. I want to make sure that certain things work as expected. So that brings us to the definition of what's called a critical section. And a critical section in the piece of code is a portion of the code where we only want one thread to be executing at any given time. So the definition of a critical section is inherently based on our idea of parallelism. If I only have one thread, the whole program is a critical section because only one thread can ever be executing that code at one time. But once I start to have more than one thread, and if I'm the operating system I have to deal with requests from all of the different processes that are running, then I come up with this idea that I want to establish points in the code where only one thread can be executing. And this is usually because I want to create the illusion of atomicity. So I have some complicated operation to a data structure I want to perform. And I can't let anybody use the data structure while I'm in the middle of performing this operation because if it's a linked data structure, I may have some links that point off into nowhere or whatever. So I'm doing something complicated to this, and I can't let anybody see it or access it, use it, while I'm in the middle of this. And so to do this, I may create a critical section. And that makes whatever set of operations I'm doing look atomic to other people who are using that resource. OK, so that's just what I said. All right, so let's look back at this piece of code. And now that we're training ourselves to think about this code as being concurrent, as being able to be run in a multi-threaded scenario, the question when you look at something like this is, what are the series of instructions that have to execute all at once? And you guys were probably thinking it's a little bit of a contrived example, and I agree it's a little bit of a contrived example, but just bear with us. So here are some other questions that you might want to think through as well. So let's start with number two. What is the shared state here? Synchronization problems frequently come about because two threads are accessing shared state. In fact, I'm not even sure if you can have a synchronization problem if two threads are only ever accessing private state. So what is the shared state here? Anybody want to volunteer, I guess? Yeah, project. The account balance. And for whatever reason, now the account balance here could be a global variable. But for whatever reason, I've decided to provide you with this interface for accessing the account balance. It has two functions in it. You can imagine this was a global variable that was being shared by the threads, that was memory, or whatever. So these are examples of shared state. The shared state could be a linked list that holds all the processes on the system. The shared state could be information about the processes memory mappings that are being accessed by multiple threads within that process, whatever. So these are examples of shared state that are going to be used by multiple threads. What is the private state here? What state here would I not have to worry about synchronizing? What's that? Large amount. OK, that's fair. Yeah, so that's a parameter to the function, right? That's good. What else? Yeah? Yeah, so this variable right here. Now, where? OK, so this is unfortunately, this question usually points out the need for a systems programming class here at UB. But who can tell me, where is that variable Gua has? Where is that allocated when this code runs in C? It's on your stack, right? So when I enter this function, the C compiler has generated instructions that allocate space on the stack for whatever global variables I need to use. And that's going to include Gua has. Has anyone ever looked at this? Have you guys ever actually taken a piece of C code, compiled it, and looked at the two side by side to try to get a sense of what's going on? Anybody? I would encourage you to do that, because it's pretty cool. You'll learn a lot that way. It's actually kind of neat to see exactly how this stuff works. So for example, the reason why, if you modify a parameter value in C, it's not modified in the caller is because that value is actually pushed onto your stack. Makes sense, and it's kind of cool to see. The tool chain that we gave you with OS 161 has OS 161 obj dump, which will take a compiled binary file and generate the MIPS instructions. Basically, the assembly code that generated that file. And that can be kind of a neat exercise. Maybe I'll do that online. OK, so now there's a part of this function that we want to make atomic. There's a part of the function that we want to establish as a critical section. Where can anyone guess what are the lines that are in that section? I think I used to have line numbers on this, yeah. The first three lines. So that would be this, this, and this. Who thinks that's right? Anyone want to guess? And there's only a couple of different options here. There's only five lines. Anyone want to make a guess about what else might be in there? Josh? Oh, OK, so someone is claiming it's only the put balance line. That's interesting. The last three lines, OK. Well, OK, so that's great. We have diversity of opinions here, which is good. OK. So it turns out that the lines that are critical. OK, sorry. This is line one. I messed up. This used to have line numbers on it. I just haven't fixed that this year on the slides. So this is line one, two, three, four. So lines two through four are the critical section. And so here's one question. So what's unique about those lines when you look at this function? Yeah. Yeah, so the answer is perfect. Those lines are accessing and modifying the shared state. And this is a very good hint that something needs to be in a critical section. It's when there's shared state. So the shared state here was the account balance. And what I'm doing is I'm pulling a copy of the balance, modifying it, and writing it back. And we already saw what can happen if I get interrupted in the middle of those three actions. And so what I want to do is I want to make those three actions atomic. So I'm going to establish a critical section that contains these three lines. Do people have questions about this? Let me just pause here for a minute and we can make sure we see how this works. Yeah, Projesh. Right, so let's go back. Well, actually, does anyone want to try to answer Projesh's question? Yeah. Right. So the problem here is that I'm making a copy of this shared state, modifying it, and then writing it back. And when I get to the point where I'm writing it back, that operation reflects my local modifications. So let's say I only had these two guys in the critical section. So now thread A runs, and it grabs a copy of the balance. Thread B runs, and it grabs a copy of the balance. And now game over, right? Because now the two threads are both going to make local modifications to that balance. And what happens is now only a question of which one gets to write the balance last. So that is by definition a race condition. There's no way that that code can be correct. Yeah, run. No, so that's a great point. So one is, now this is an interesting question, OK? Because this is kind of a correctness versus user interface. So in theory, you can imagine that this function sends me a text message or something like that, or sends a plane up into the sky to skywrite the amount that's in my bank account or something like that. So this is going to notify me of the amount that I have. Now what happens if I don't put that line in the critical section? Well, OK, so hold on. So I think it's pretty easy to convince yourself that this function will get called twice. So if thread A and thread B run, no matter in what order they execute the first three lines of the function, they will both call that function. Now if I don't put it in the critical section, what could happen? Let's say we have the previous example where one person's bumping the balance from 1,000 to 3,000, and the next one from 3,000 to 4,000, yeah. Yeah. So exactly. So I might get the notifications out of order, right? And that's assuming that whatever notification mechanism here is actually going to preserve ordering, you know, again, maybe one of the skywriters is lazier than the other and he takes like an hour to get up or something like that, or he can't find me or he doesn't bring the right color smoke or something like that, right? So you have to assume for this to make any sense that the notify function actually, those are gonna kind of reach me in order, right? But if they did reach me in order, what could happen is that even in the case where the balance is correct, I get a notification that says your balance is 4,000 and then the next notification is your balance is 3,000. So that could be confusing for me. And I might think that someone was about to do very poorly in the class indeed if they deposited negative $1,000 in my account, right? But it wouldn't affect the balance, right? The balance is right. So when I get done freaking out and actually go to sit down on the computer and check my bank balance, they go, okay, right? That just turned out that that was tricky. But again, it's interesting to think about kind of what you can guarantee and what you can't, right? Because the notification, I would argue, is unlikely to get done right. I don't know what banks do about this. Does any, I don't know. This is just a totally random question. Does anyone's bank actually send them this kind of notification? Yeah, does it work? Oh yeah, so that has never made any sense to me. Like why do credit card things take so long to process? I don't get it. It's like, it's, you know, and then sometimes they go through and then they vanish and they come back again later, like a month later, right? Right there, it's always a little easier. What's that? The code is right there. I know, the code is right there. Come on, guys, like take my class. That way you can fix your stupid bank, right? By the way, just, I just want you guys to understand if you're in this room and you're in this field, like you guys are the future rulers of the world, right? And one of the things you're gonna do is get rid of banks. Thank you, okay? There's no reason to have this physical thing that's a bank. It's probably this, like banking is almost over and thank you, right? Anyway, and then what will happen is instead of it being some idiot, you know, billionaire's fault that your poor, some hacker will screw off and there'll be a backdoor in the software and your money will vanish, right? Sort of Mr. Robot plus plus, right? Okay. All right, so, does this make sense? Are we good here, right? So, Ron makes a great point. I could put that in there. I could put line five in there and that might make me feel good. It wouldn't hurt anything. It would make sure that the notifications get fired in the right order. In what order they reach me? Who knows, right? And I'm assuming this is why banks don't send you email every time your balance is updated because it would be confused, right? Yeah? Could you rewrite it in a way where putting your balance you can add to it so that this would be flexible? Yeah, but then we wouldn't have this problem to talk about, right? But I'm just asking, would that be a better design? It could be. Now, I wanna point out something though, which is something that you guys should be really careful of. There are things, so, in general, another assumption you cannot make about atomicity is things aren't atomic just because you could write them in one line of C code, all right? Even if it's like I++, you have really no idea what instructions the compiler is going to generate at that point. So do not make any funny assumptions about, oh, well, it was one line of C code. So therefore, it must be atomic. No, not true. So you always have to synchronize and create these critical sections if you want things to be safe. Even if all you're doing is adding and subtracting stuff from a global variable, if you've got a bunch of threads that are doing it at the same time, something will go wrong, right? Sleep better at night, put in the synchronization that you need. Okay, so, when we start, let's start, so we've introduced this concept, just this idea of a critical section, how does it work and what are the design requirements? And then we'll talk about how we're gonna actually implement it. So the simplest requirement is that only one thread should be executing in the critical section at once. So conceptually, if I look at that C code and I say I'm gonna establish a critical section in this area, what it means is over all of the code where multiple threads are executing, you could have lots of different interleavings and you could have two threads, eight threads, 10 threads that are executing the same line of code or different lines of code in the program. But in that one area, there's only one thread executing those instructions at once. So a thread goes in at the top and once there's a thread inside of the critical section, nobody else can get in there. The second thing that's pretty important is I actually have to be able to make progress while I'm inside the critical section. If I don't, then it's possible that I'm the last person who will ever go into that critical section. And the last thing is a performance consideration which is, again, go back to the, I mean, what's the simplest way to make your code safe? Run it on a Raspberry Pi with only one core and then you have no problem, right? But that's not interesting, right? That's not what we do in 2016. We have machines with 128 cores and everything needs to go screaming fast all the time. So in general, you want as much concurrency as possible. Now, in this class, you may be tempted to make things bigger. We have had people that have submitted working versions of assignment three that had large locks around their entire VM. That's okay, right? I mean, if it's what you need to do to get it to work, I'm okay with it. You will feel better about yourself, however, if you have thought through the problem in a way where you can actually support more concurrency. And in the future, it'll be required, right? But when in doubt, if you're trying to get something to work, you can't quite think about exactly how threads are interleaving, making the critical sections a bit bigger can be helpful. Okay, so now how do we actually get these things to work? So this is all conceptual at this point. So there are two ways to implement a critical section. The first way is that once a thread enters it, it cannot stop executing. So we have to stop the thread switching that would normally go on and make sure that that thread is the only thread that can continue to execute. The second way is we create some mechanism so that a thread cannot get into the critical section even if there's another thread inside of it. So with the first mechanism we're relying on, our ability to continue to execute. In the second part, we're relying on some other mechanism that's going to track when a thread is inside the critical section and make sure that other threads don't execute. And so in the second mechanism, the thread that's inside the critical section can actually stop executing. It can do things that take a while. And it's safe because I have some tool that's allowing me to keep other threads out. Okay, now I don't know. It's probably getting to be time for me to remove the on-uniprocessor as parts of the slides because who cares about uniprocessors, right? So I'll skip this. So in general, the first idea about stopping other threads is broken because so on a multi-core system that would essentially involve shutting down everything else, right? Even if you could get this to work at all which would be tricky, you'd have to stop every other core on the system. And that would be sad. I mean, you paid for those cores, you want them to do stuff. So if they're sitting there idle, you will probably be upset. My brother went to, my brother's a freshman in college. His roommate had a multi, it was a multi-processor Windows machine. This was pretty exciting. This was like the first time that they were shipping these consumer machines with multi-processors in them. So there's two processors. I think he paid like a bazillion dollars for the thing. You know, he was thinking about going into graphic design or something. So he had this really high-powered computer. There was only one problem. He was running Windows 95 or something. And Windows 95 did not turn on that other core. So that other core was just sitting there, hanging out, cold, dark. And it was a little bit of a headscratcher for me how whatever unnamed computer company that sold him this machine would have allowed him to configure a machine where there was $500 bill sitting in there just slowly being roasted, right? By the other Pentium 4. I didn't really get that. But anyway, I think eventually he upgraded to Windows NT and everything was better. But it was kind of funny. Okay, so one of the... So at some level, I mean, here's the other thing that's gonna start to happen here is, and this is one of the first times that we see this in this class, but there's this really beautiful relationship between operating systems software and computer hardware. And this is a symbiotic relationship that's been going on for decades where software designers think about ways to make the hardware perform better and hardware designers sometimes, if they're in the mood, provide features that make software development a little bit easier. And particularly on multi-core systems, getting cross-core synchronization to work really requires some amount of hardware help. It's very hard. I think it's possible to do this. I think it's maybe like a Google interview question how to get some of this stuff to work on multi-core machines with no hardware support. So I think it's theoretically possible but it's usually pretty inefficient. And the better way is to ask hardware to help us. So these special hardware instructions manipulate, usually manipulate memory. And they manipulate memory in a way that the hardware guarantees will have certain properties even if multiple cores are accessing that memory. So what's the, you know, people that have taken courses on hardware, what are some of the usual issues with concurrent access to memory on multi-core systems? So it doesn't seem like it's a big deal, right? I've got four cores, I've got four gigs of memory, and those cores are just reading and writing to the memory and everything's fine and happy and all those memory reads and writes just proceed normally. Is that how things work? Somebody knows this and is gonna tell me about it, I'm sure. Cash, yeah, exactly. There's a famous statement, there's a famous computer scientist said, there are only two things that are hard at computer science. One is naming things and the other is cash management. So yeah, these processors, they're not just writing to main memory, they have a cache of memory. They have an L1 cache or an L2 cache. Depending on your architecture, they might have an L3 cache. Depending on your architecture, they might have a private L1 cache, an L2 cache that's shared with two cores, and an L3 cache that's shared with eight cores or something like that. I mean, you have all these really wild cache architectures and all of them are basically designed to just make main memory seem fast because it turns out from the perspective of the processor, main memory is really slow. So think about that. Processor is so fast that even memory is slow. So I need somewhere to put things. Now, if I have two processors that have private caches of memory contents, what problem does that create? So let's say you have two threads and they both write to a, let's say that thread A writes to a location in memory and then thread B reads from that location. What do I expect to happen? Yeah. Yeah, so what happens if thread A and thread B are both caching that value on the core and so that caching can sometimes conflict with coherency? So one of the reasons why you guys might be thinking, well, I've got four core systems. Now I've got eight cores. How many people have a machine with more than eight cores in it? Yeah, sweet. 64 cores on my desk. It's awesome, right? Yeah, so I mean, but clearly that's Moore's law, right? Like we're gonna, you know, a couple years from now, we're gonna be at 1,024 cores. So it turns out one of the things that holds that vision of the world back is that shared cache management on those cores is, goes as n squared. So it gets worse and worse and worse. The overhead of providing this abstraction of sort of shared coherent memory, right? Which is what you guys are used to. You guys don't realize how used to you are to that abstraction, but imagine trying to program a machine where each piece of memory has sort of different properties to it. That would be weird, right? Be like, well, if core one writes to the memory and core two reads it, you're okay. If core one writes to the memory and core eight reads it, it's not going to get the value, right? That would force you to rethink some of the things you do. Okay, so, but in order to get some of these, so these primitives tend to be pretty high overhead when you look at their impact on the cache because they have to make sure that stuff is synchronized very carefully across all the cores. So there's a couple of variants of this, right? The first one is something that's called a test inset. Now this is C pseudo code for this instruction. Keep in mind, this is implemented in hardware. This is a hardware instruction. And what it does is it sets, you tell it, here's my address of memory. And what it'll do is synchronously do two operations. It will set the value and return the previous value. So if I test inset memory address that had a value of 10 with the value 12, it gets set to 12 and 10 gets returned and that happens synchronously. So if two cores are doing the test and set at the same time, one of them will get one value and the other one will get the value that was just written, right? All right, so the other version is something called compare and swap. And again, this is sort of C pseudo code for what the hardware does. So I compare the contents of memory with the value that I pass in. And if they're the same, then I set the value to some new value that's provided. And again, this is something that is done atomically. So this is a single processor instruction. And these processor instructions have these special properties that they are guaranteed to have certain properties even if multiple cores are using them simultaneously. Yeah. Why is it called swap? Because I swap in the new value. I compare with value A and if they're the same, I swap in value B. Yeah, I didn't come up with the names. So don't blame me. Okay. And then the last one, and this is actually what, and the reason I put this on the slide is this is pretty much what OS 161 provides is a combination that's called load link and store conditional. So load link returns the value of memory address. I always have to read this one because I forget exactly how it works. And then the following store conditional succeeds only if the value has not changed. So you can think of load link as kind of saying, I'm preparing to do this store conditional. And then the store conditional will, it will only succeed if somebody else hasn't done a store conditional in between the two. And this is actually the code from your OS 161 kernel that's, I think this is used in the spin lock, in the spin lock implementation that we give. Okay. So most, I shouldn't say most. I mean, pretty much all modern processors provide some variant of one of these instructions that's not several. And the reason is to enable multi-core synchronization inside the operating system. And in other cases, and pretty much if you have one of these you can kind of make the others if you want them for some reason. But you can use any of these as the basis for what we're about to do. All right, I'm just talked about this. Okay. So let's try to use one of these low level primitives. Now this is not what we would normally do. What you guys are gonna do for assignment one is build higher level primitives that are gonna use these internally. But let's just try to use one of these directly, okay? So let's take our previous example and let's modify it to use a tested set. Remember, so a test and set tests a variable and then returns the old value, right? So here's what I'm gonna do. I'm gonna use a test and set to indicate the fact that there's a thread inside the critical section. So at the top of the critical section I'm gonna set the test and set that says, hey, I'm inside and then at the bottom I'm gonna clear the test and set. So does this work? Anyone wanna argue that it works? Just kind of like a little lock. Yeah, why does it work? Since it's semaphore, it's, oh, okay. So you'd be careful with the interface here, right? It's, it might look like a semaphore. What does this code not do? Yeah, there's like, all I do is set the test and set. So let's say thread A comes in here and sets the test and set and then thread B hits this line. What is he gonna do? Set the test and set and just keep on going. So clearly I need some kind of conditional logic here. Yeah. Yeah, progestures B to me to the punch line here, right? Okay, so let's try this again. So I need to do, so I need to check the test and set. So what this says is if the test and set is already one, right, remember the test and set returns the previous value. So if the test and set returns one, then there's already someone who's set it, right? Now what do I do here though? Sleep, yeah, I wish. I haven't told you guys, there's no way to sleep, right? There's no way to sleep here. I haven't given you guys any primitives by which to sleep. And keep in mind, when I get to the bottom here and that guy hits that test and set, all that's gonna happen is a memory location is gonna change value. So what do I have to do? Yeah, check it again, okay? And this will work, right? So this is a solution to this problem. Now, what's the problem, so does everyone understand this code? At the top, I check the test and set. If it's one, I check it again. If it's one, I check it again. So this will work, what's the, well there's one case in which this will not work. What could possibly go wrong here? So yeah. Or in certain cases, it's possible that that thread that's checking it over and over again is can prevent the other thread from actually making any progress. So let's say thread one gets in here, sets the test and set, now he's busy changing the balance. Thread two now gets to run, and he just sits there spinning. He's waiting for something that's never going to change until he stops and lets thread one run. Yeah, project. So okay, so that's a great question. What happens if I have one thread, it gets inside the critical section, thread two comes and it starts to spin, and then thread three comes. What's going to happen? So thread two and thread three are just sitting there, and maybe they're on two different cores, so they're actually running at the same time, and they're just banging on that test and set. Bang, bang, bang, bang, bang. At some point, thread one runs, hits the bottom here, clears the variable, then what needs to happen? Somebody is going to get a zero, right? And keep in mind that the test and set instruction has to make sure that only one of this happens, right? So this is why this is special. Even on a multi-core machine, the test and set instruction has the semantics that it will ensure that even if there's 10 threads sitting there spinning on 10 different cores, only one of them will get zero and proceed. The rest of them will just keep waiting, and then one by one as the threads get done updating the balance, they'll proceed through the critical section. Okay, so, and this is an approach that's known as busy waiting, right? Because I'm busy waiting. I'm waiting by just doing the same thing over and over again. And you can imagine this isn't particularly effective or efficient because, I don't know why this is here. Just, whatever, I just felt the need for a meme in the middle of the slide. So when, so the busy waiting that's going on here means that there's this wasted resource because I'm consuming CPU cycles and I'm not doing any useful work. The waiting for the test and set to be cleared is not, it's not qualify as useful work, yeah. Oh, sorry, test and set, yeah, yeah, TAS. Now, so on a multi-core system, this is bad because I've got multiple cores that are spinning. On a single core system, this is terrible. Now again, I don't know why I'm talking about single core systems, but even on a multi-core system it's possible that the thread that's spinning is preventing the thread that needs to drop the test and set for making progress. So it's doubly stupid. Not only are you wasting resources, the best thing for you to do would be to stop and let that other thread make progress so it can clear the test and set and you can get into the critical section, okay? Yeah, you have a question? Yep, yep, no, remember, remember the semantics of the test and set are that when the test and set goes to, so if the test and set is one, I'm going to wait. When the test and set goes to zero, it will only return zero once. So no matter how many people are waiting, somebody's going to get zero and drop through the while loop and then at that point the value is not one. No, no, no, because I'm testing it and setting it to one. Remember? Yeah, yeah, so the test and set says set it to one only if the previous, it basically says set it to one, return the previous value. So as long as it keeps returning one, it's locked, right? It's being used. As soon as it returns zero, I fall through but I set it to one, right? Yeah, that's important. Yeah, nothing, I mean, but remember, let's say I drop through and I'm right here, the test and set is now one. Exactly, right? So when this while loop completes, the value of the test and set is always one. That's critical because that's essentially what I'm using to indicate that there's a thread inside the critical section. Okay, let me see where I am on time. I don't know if we're gonna do locks today or not. Okay, so let me get a couple more minutes. So the general primitive used to implement critical section is what's called a lock. And this is what we've been using, right? I mean, if you change around some of the terminology, a lock indicates that when I'm in the critical section, the critical section is locked. When I leave, I unlock it. So threads acquire a lock when they go through a critical section and they release a lock when they leave a critical section. Now locks are, I just wanna point out, locks are more flexible than this. So I can use the same lock to protect multiple critical sections. I can use the same lock to protect multiple pieces of code that all have to be synchronous with respect to each other. So frequently, if I go back to the example of a complex data structure like a linked list, I might have an insert operation. I might have a remove operation. I might have an append operation. All of them might acquire a lock on that data structure so that they can work safely and not worry about, if one of them's trying to append and the other's trying to remove and I end up with a cycle or something ugly. Now what we've implemented today is what's called a spin lock. So a spin lock, it works because through busy waiting. So when I can't acquire a resource that's locked by a spin lock, I continue to wait actively for that resource to be freed. Now it is not, spin locks are not inherently bad. I'm sort of maligning them right now. They're rarely used as a higher level primitive to solve synchronization problems. So the stuff that we're gonna give you guys to do on assignment one, we're not gonna ask you to solve with spin locks. But they're regularly used to build more useful synchronization primitives. And there are certain cases where spin locks are appropriate. And what we'll do is pause for today. You guys can go enjoy the Super Bowl. And on Monday, we'll talk about why spin locks might be useful in certain cases.