 All right, good morning, everybody. I hope everybody had a nice weekend, stayed inside, enjoyed the snow to the degree that that was possible. OK, so last week, we started talking about threads. And we spent most of the week talking about threads and how the operating system abstracts the CPU. So this week, and maybe a little bit of next week, and assignment one are about some of the primitives and support that the operating system includes for dealing with the consequences of threading, the problems and the challenges associated with threading. And programming multi-threaded applications successfully is important to really writing any modern software code, any modern computer code, any type of application you guys write in the future will likely be a multi-threaded application. And you're going to deal with some of these problems. However, the kernel itself, the operating system kernel, is a fairly important multi-threaded application, and one that's very difficult to get right. And even if that seems maybe somewhat unimportant, the kernel is also what you're writing this semester and what will dictate how well you do on the assignments that we give you. So getting multi-threading right in the kernel is pretty important. So we're going to talk a little bit about how to do that. And recitations this week and next we'll focus on that. And Assignment 1 will give you guys a chance to practice that and to build some synchronization primitives that you guys will use later, all right? And let's see if this is going to work today or not. All right, so yeah, the slides and video hopefully will be up this week. All the assignments are now posted. And at some point this week, we'll finish the Assignment 1 auto grading, so that will start to work as well. So Assignment 3 has been posted. The code reading questions, I think, for all the assignments are submitable. And Assignment 0, the auto grader, has worked. We're fixing bugs with things. So people notice problems with the GDB script grading, so we fix that. And some number of people got one point back. Every point matters, so I'm not going to. But so yeah, if you guys see problems, I mean, look, the scripts are done. Like they're little Python scripts that are doing their best, right? But there are corner cases that they probably won't be able to handle if you notice them, and you think you did something right, didn't get a point, or multiple points for it, please let us know and we'll work on fixing things. OK, so any questions about last week's materials? Last week, we did CPU abstractions. We talked about some of the implementation required to move threads on and off the CPU. We talked about thread state. We talked about thread abstraction. Any questions about that before we plow forward? I'm going to do too much review today. All right, let's do a little bit of review. So context switching. What is a context switch, Nathan? OK. I brought the idea to the end. OK, so context sweat is the process of switching from one thread to another, right? This can be, as Nathan pointed out, this can be something that is done by the kernel. It can be something that is done by user space libraries, right? Nathan started to talk about the state that we need to change out. And that private thread state consists of what? Jeremy, were you going to answer that question? Do you have something else? Registers, right? So what did the CPU look like when the thread was used in it? How did the thread leave the CPU? And what's the other important piece of private? Typically, what we think of is private thread state. Yeah? Stack. The stack. What's her name? Isis. Isis, right. OK, stack and registers, right? So the registers are what's on the CPU and the stack is this area of memory that's typically considered to be private per threat, although many times the operating system doesn't actually enforce that. Threads can write into each other's stacks if they want to in user space, but that typically doesn't work out all that well. Unless you're very, very clever about it, right? So why do we do this? Why do we switch between threads? What's the goal? Parallelism or my favorite word, concurrency, right? We're trying to create the illusion of concurrency. We're trying to make the machine look like it is doing more things at once than it can possibly be physically do. So on a unit processor machine, it can't be doing more than one thing on a multicore machine. I'm limited by the number of cores, but the operating system is clever enough and smart enough. And if it does this well, it can make the machine look like it's doing many number of things all at once. All right, thread states. So we'll spend a little bit of time here, right? So we covered this right at the end of the class. So what do we mean when we say a thread is running? Yeah, it's actually using the CPU. We're executing the instructions associated with that thread and those instructions are modifying the state of the machine and the state of the CPU. Pretty simple, right? What do we say when a, why do we refer to as thread is ready? What does a ready thread mean, Gina? It's waiting, but yeah, so it's not running, right? So again, we're sort of going down here. It's not running, but how do we distinguish between the ready state and this other state? What's the difference between the two of them? Right, and so what could it be doing at this exact moment? What is it ready to do? Run, it could be running, right? A ready thread could be running on the CPU. It isn't, but it could be, right? There's something stopping it. And then waiting, blocked, or sleeping indicates that this thread is what? It's not ready to execute. There's something that needs to happen, right? Something that is asked the system to do some, and we'll talk today about other things that could prevent a thread from running, right? But there's some reason that this thread cannot run at this time, okay? All right, we didn't get through this last time, so let's talk about the valid thread state transitions, and there's a doc diagram that we could put up here too, but I didn't do that, right? So these are the transitions that we typically think of as valid transitions between different thread states, right? So when a thread goes from running to ready, right? So a thread was running, now I've moved it, the operating system has moved in into the ready state. What do we call, what do we call that? What do we call that action, that process? AJ, I was running, now I've been, I'm not running anymore. I've been moving to the ready state. What has the operating system done to me? Well, a context which has taken place, right? But for this particular thread, we say that this particular thread has been Paul. Yeah, descheduled, right? The thread was descheduled. It was running and I said, you know, your time is up, I'm gonna run something else, right? So we call this thread being descheduled. What about moving from the running into the waiting state? So a thread was running, something happened, and now it is waiting, so now it's no longer ready to run. What happened, okay? Yeah, but it was running, so now it's waiting, so what must have happened? Yeah, it did something. A system call is a good example, right, Greg? Yeah, I mean, something happened. A lot of times this happens because of a system call, right? But the thread did something that caused it to have to wait for something else to happen, right? And one example of that could be, you know, a blocking system call, right? I made a system call, I asked the operating system to read some data from a disk, and while that data is being read, because of the semantics of the system call, I can't start running again until that read completes. And so until that read completes, I'm not ready to run, so I've been moved into the waiting or the blocked state, right? What about from waiting to ready? So I was waiting, now I'm in the ready state. What happened here? Greg, okay, so the second half is right, the first half, not so much. So the resource is available, right, but am I running? Have I moved into the running state? No, I'm now put back on the ready, you know, some frequently operating systems maintain state, including your operating system, maintains these threads and queues, right? So this means that I've been moved off the waiting queue or some sort of wait channel, which is actually the specific name of the abstraction your kernel uses. I've been moved off some wait channel, and now I'm on the ready queue, right? Yeah, do you have a question? Yeah, go for it. Yeah, whatever the thread was waiting for happened, right? So the discrete is done, the network packet arrived, whatever, right? Okay, what about ready to running? Go back and get the first half of Greg's answer. Yeah, Tom? This is called being schedule, right? The operating system chose me to run. It said the time has come, you've been on the ready queue, I know that you're ready to be run, and for whatever reason I've decided to schedule you, and we'll talk more about scheduling policies in terms of how we decide. So this is scheduling policy, right? Scheduling policies, looking at the ready queue and deciding what threads, or what thread or threads if you're on a multi-core system to run, right? What about running to terminate it? What could have happened here? A couple of things that could have brought me through the state. Daniel? Yeah, I could have done something naughty, I could have done something bad that would have caused me to the operating system to terminate me. What else could have happened? Yeah, I just might have exited, right? I might have said, eh, I'm done, right? So I exited or perhaps I hit a fatal exception that caused the process to end. And again, this is another thing you guys will implement for assignment too, right? Is both thread exit, and I'm trying to remember what the name of it is, that the thing that handles fatal exceptions that are hit by threads. I mean a brain freeze, I can't remember what it's called, but you guys need to implement that. Currently it does something drastic and not very good. All right, a little bit of review, a little bit of new stuff. Questions about this before we talk more about synchronization and concurrency. All right, so, I mean, concurrency, real concurrency and the illusion of concurrency that the operating system provides are really helpful and useful things, right? It gives me a powerful way of thinking about how to structure my applications, right? I can write multiple threads that are executing quote, unquote, in parallel, one that's waiting for the mouse and other one that's rendering the page, whatever. And at the operating system level, the most fundamental thing is it allows me to switch things back and forth in order to hide the latencies that are caused by slow hardware devices. So I don't have to let something sit there like I would have to on a batch processing system until the disk completes. I can stop it, get it off the processor, run something else and make sure that I try to keep the processor busy, right? I try to keep the processor busy. I try to keep the memory busy when things happen that take a while, whether it's waiting for IO or waiting for you, right? To push a key or to realize that some input has occurred. I can do other useful work, right? Unfortunately, concurrency also creates problems, right? And I would argue, I mean, one of the big problems with concurrency is the fact that you guys are trained to think about programming and it's natural to think about programming as a sequential series of steps, right? And frequently, as programmers, we're not used to this idea that our program can be stopped at any time. So we do these things that require multiple things to happen kind of all at once and we don't realize that if we're stopped in the middle then we've left things in an inconsistent or dangerous state and if somebody else observes them then bad things can happen, right? So we're gonna talk about two different, we start to talk about synchronization primitives and synchronization tools and ways of thinking about synchronization. We're gonna, there's two primary things that we think about, right? One is correctness. And correctness is particularly important. Correctness is the idea of ensuring that there's certain variants in the system that are maintained. Ensuring that certain processes either happen or don't happen. Ensuring that there's state in the system that is kept correct, right? So if I have a count, for example, of the number of threads on the system that count should be correct, right? That count shouldn't slowly drift upward because I have some sort of race condition that's causing certain threads not to be removed, right? And that thread, that count shouldn't go negative, right? So you can express different invariants about how you watch your system to run and frequently when there's multiple steps that are required to adjust certain things about the system correctness starts to come into play with respect to synchronization and we have a great example of that today. Concerns my bank account, it's like the classic. The classic correctness example, the professor's bank account, right? Clearly the most important thing to keep correct. All right, so coordination is another aspect of synchronization, right? So this comes down to how do we enable safe communication between threads? So once threads start to run together to do something or have to access shared state, we start to have a problem of communication, right? And there are certain types of synchronization primitives that we're gonna talk about, for example, things like LOX, right? That are really largely correctness-oriented primitives, right? There's implicit scheduling that LOX enable, but it's not explicit, right? And then there are other types of synchronization primitives for example, something called a condition variable that are really communication-oriented, right? They are designed to allow processes to communicate with respect to shared state, right? And they give you nice design patterns that allow you to manipulate shared state in safe ways and make sure that different threads that are accessing it know what to do when certain changes take place, right? So these two things, frequently, I think it kind of mixed up together into this synchronization stew, but we're trying to keep them a little bit separate when we talk about them, because it's worth thinking about these separately, right? So correctness is when we're modifying things together, how do we keep them correct? Coordination is we're modifying things together, how do we communicate so that those modifications take place efficiently? And when multiple things are, multiple threads are trying to access the same data, right? And today we're gonna look primarily at correctness, right? So one of the reasons that, you know, how many people have done multi-threaded programming before? Great, so you guys have all done this and that's great, right? And I don't put anything past the experience you guys have had in other places, but I'm gonna claim that the operating system is a more difficult program, a more difficult program to write in a multi-threaded way than a lot of the things you guys might have been used to, right? And there's a variety of reasons for this, right? Does anyone wanna speculate as to why the operating system might be a more difficult multi-threaded application to write, to debug, to work with? Yeah, yeah, okay. I would say that might be true, right? I mean, let's put it this way, right? Certainly, unless you're using user-space threads, the number of threads that the operating system sees is strictly greater than the number of threads that are in use by any user program running on that system, right? Assuming, again, that they're all visible to the operating system, yeah. Yeah, right, so when you guys are writing multi-threaded user programs, the operating system, assuming the operating system is programmed correctly, is still isolating those threads within your process from lots of other types of damage they could do, right? And actually, it's doing two things. First of all, it's helping you debug your program, right? We'll come back to this when we talk about things like, you know, core dumps and segmentation violations, right? The kernel doesn't have segmentation violations. The kernel just messes things up when that happens, right? The kernel doesn't have a way of defending itself against its own problems, right? So rather than dying and saying, hey, you have this bug right here out this line of code, you can load it up in GDB and figure out, hey, I know a pointer exception, the kernel just assumes that you really want it to write zero into that weird memory location and it'll just do it, right? And who knows what will happen next, probably a bunch of things, and then later your kernel will crash and you'll be like, huh, I wonder what happened. So yeah, that's definitely true, right? So the kernel, doing multi-threading in user space means that you have the operating system helping you. You have it keeping your threads from destroying other processes, but also more debugging support within that application. What else? What else is hard? Swetha, she wasn't raising her hand, she was just waving it near. Okay, I mean, I think that could be true, right? So the scheduling problem, we try to isolate, right? So remember we have this policy mechanism split that we like to do in the kernel. So scheduling I think is a hard problem, right? I don't think it's necessarily a hard synchronization problem. Yeah, Paul. Yeah, there's a lot of shared state, right? Remember, the operating systems, what are the two things the operating system is doing? Right, two main goals of the operating system. Andrew, give me one of them. It's multiplexing resources, right? And abstractions, right? Those are two big things, multiplexing and abstractions. Multiplexing is all about dividing up resources and enforcing those divisions. And you're dividing up resources between a bunch of other things, right? So essentially to the operating system, the entire machine is shared state that it's trying to divide safely to the processes that are running, right? So it's like everything's shared state, right? Everything the operating system is doing is trying to enable safe sharing, right? There's very little natural isolation that goes on here. It's all shared state, right? Yeah, so I think we got most of these, you know? It's multiplexing hardware. It frequently uses a lot of threads, right? Someone pointed this out. And it does this, some of those threads are kind of natural extensions of user threads and some of those threads are created by the operating system. I mean, if you run PS or other types of process introspection tools on your system, you'll see that your kernel frequently has a lot of threads that are running, right? Some of those threads are doing things in the background. Some of them are responding directly to hardware requests or software initiative requests, right? There's a lot of multi-threading going on. And these two things kind of combine to give us this very, very hard part, right? And the lack of debugging support doesn't help and et cetera, et cetera, right? So again, and yeah, somebody pointed out, when you get this wrong, just like really weird things start to happen, right? Race conditions in the operating system are very, very difficult to get right. So remember we had this, you know, and this is something, again, just to keep in the back of your mind whenever you are writing code for this class, right? Unless you've done something, unless you've taken proactive action to make sure that this doesn't happen, what is the case about your seemingly sequential program? All right, there were a couple of things we pointed out. I've got a thread that's running along, and then what? Yeah, right, so if I have multiple threads, they can run in any order. And again, unless I've done something to prevent this from happening, what can happen to a thread that's running? Be stopped, right? They can be run in any order, they can be stopped and restarted at any time, right? And they can remain stopped for an arbitrary length of time, right? During which time a lot of things can change, right? A lot about the state of the world can change. And keep in mind that these are not, you know, I mean, maybe this is obvious, but these are not things that the operating system is doing to mess with you, right? They're done because the operating system is trying to make efficient use of resources. And frequently that means stopping things and creating the solution of concurrency so that I can hide these delays caused by slow hardware devices, right? All right, so let's look at again, so the canonical professors' favorite synchronization example, the professor's bank account, right? So I've got a thousand bucks, that's on a good day. And two of you are trying to make concurrent deposits, right? So one person is depositing $1,000 and one person is depositing $2,000, right? This is, you know, we talked about at the beginning that we're gonna grade with these two axes. This is the third axis of grading in our three-dimensional. So this is what pushes you up to the top in that third axis, right? So, okay, so someone is spending $1,000 and another person $2,000, okay. So here's the code that you're running, right? This is a little bit of pseudo code. You're gonna grab a copy of the balance, you're gonna modify that copy, and then you're gonna return it and then you're gonna notify me that I'm rich or richer than I was. It's all relative, right? Then what Ross Perot said, if you have one penny and someone gives you another penny, you've doubled your net worth. But you still only have two pennies, okay. So what can happen here, right? So here's the best-case scenario, right? Somebody, you know, first person who comes in, code runs, their little thread code runs to completion. At the end of this code snippet, I now have $2,000. And this person now has a B, right? Second person comes through, at the end of their, when their thread runs, at the end of their thread, I now have $4,000, this person has an A minus, right? Okay, so how many different, I guess that's a good question. How many different answers can I come up with for this? How many people wanna guess? What are other amounts that could, that I could have at the end of this code snippet? Three, what are they? Okay, so that's the one I just did, right? Okay, okay, that's all right. So here's the case where things again go less well for me, right? So now I'm into the 3,000 range, right? So again, I mean, this is the type of thing you guys have to think about. Whenever you're writing code, right? It's this type of interleaving, right? So thread one starts to run or the pink thread starts to run and it gets a copy of the balance. Now the blue thread runs and it gets a copy of the balance, right? Because there's no explicit synchronization here, they both have 1,000 as the balance, right? Now the red thread runs and deposits $1,000, increments the balance to 2,000. And now the blue thread runs and deposits $2,000 and increments the balance to 3,000, right? So at the end of this, I have $3,000. So this makes sense to everybody, okay? And then again, this is the worst case scenario, right? Where all I have to do is switch the ordering of the two threads. In this case, the red thread and blue thread both grab the balance at the same time. Again, they both get $1,000. But this time the blue thread deposits first. And here at the end of this, I have $3,000. Now the red thread runs and deposits $1,000. Here I have $2,000, right? Okay, yeah? Why can't you have, I mean, if the blue thread runs? Yeah, yep. So the blue thread is the only one that's going to increment this to be 3, right? So if the blue thread ran twice, I could have 5, right? Yeah, but I'm assuming that each one of these threads is going to run once, right? They're each doing one deposit, right? Yeah? Through with it at the first time. And then the pink thread gets the balance. Yep. And then it increments. So let me go back to the first case, right? So this is the case. This is the non-interleaved case, right? In this particular case, I went from one to two to four. If I switch the ordering of threads, I'm still going to end up with four, right? I'm going to go from one to three to four, right? So what's the? What I'm saying is the blue thread is running first, right? Yep. And if it goes through the whole thread, and then the pink thread comes in. Yep. So the pink thread will have 3,000 as balance, right? Yeah, and it'll give me one. The pink thread is giving me 1,000. So if the pink thread comes through and it's 3,000, then the result is still four, right? Yeah, yeah. Yeah, no, it's fine. It's good to think about these things. In the non-interleaved case, it doesn't matter which order the threads run, right? Like that, you know. But as soon as the threads start to interleave their execution, then it does matter, right? OK. So who thinks they can, I mean, you guys have done multi-threaded programs. So who can define what a race condition is? Nice rigorous definition of a race condition. I got someone in the back of me. Yeah. Yeah, OK, well, how? What's specifically about how? Wembley. Yeah, so really what it is, when the output of the process is unexpelling dependent on timing, right? Or other events, but normally we think about this in terms of timing. So if there's something, and this is why it's called a race, right? Whoever wins, and some definition of win, right? And it's normally not that simple because the threads are starting to stop being, but whoever wins determines the output of the process, right? Something about timing makes the output of the process dependent on that. And that's not what we expect, right? And this is important, right? So whenever you guys start to write code and you're thinking about race conditions, you have to think, what do I expect to happen, right? What is supposed to happen here, right? What I expected to happen is that I would have $4,000 after those two deposits, right? If something else happens, then there's a problem with my code, right? And in particular here, that problem is a race condition. A race condition is not the only thing that would cause my expectation of what would happen to be violated. It could be programming bugs and other things, right? But in this case, what I do have is a race condition, right? All right. So what we start to develop here is this tension between concurrency, which is this illusion, or again, in multicore the reality, there are multiple things happening at once, and atomicity, which is the illusion that a set of actions that actually took place separately happened all at once. So when we start to talk about a series of actions being atomic, there's frequently two ways that we can mean that, right? One way is that they either happen or don't happen, right? So when people talk about atomicity with respect to things like database transactions, what it means is that if I start a series of instructions, modifications to the database, they should either all happen or none happen, right? They don't always all happen. Sometimes there's been some problem that causes me to not be able to execute that particular atomic set of instructions. And I have to roll back and restore the state when I start it. But the idea is that it either executes or it doesn't. When we look at synchronization and concurrency frequently, what we say is that no thread could see the result of, could see some of these things in an inconsistent state, right? So global variables and other state that threads share with respect to atomic action, to me, if an action is atomic, all of the components of it that frequently mean modifying multiple pieces of shared state in some sort of well-defined way happen kind of at once from the perspective of another threat, right? So no other thread will ever be able to see, sometimes see, sometimes modify the state when it's in an inconsistent state, OK? And concurrency, again, it's interesting. So concurrency requires the ability to stop and start threats at any time in theory, OK? In order to allow us to develop this idea of atomicity, we have to start rolling back the idea of concurrency. So I have to start saying, I can't start or stop a threat at any time, right? I'm going to start to chip away at this idea of concurrency in order to give myself some more safety and allow me to coordinate the behavior of multiple threats. So this is important to remember, right? If without some control over concurrency, I can't achieve atomicity and I can't build any of these synchronization parameters we're going to talk about, right? OK. So this is just an interesting aside, right? So there was one of the first operating system designs was put forth by Dijkstra. How many people have heard of Dijkstra before, right? How many people knew that Dijkstra wrote an operating system? Not very many people. Yeah. So Dijkstra is more well known for perhaps other things. But Dijkstra did put forth the design of what he considered to be a provably correct operating system. This was back, I think, in the 60s. Man, I should know this stuff. That operating system, the story I've heard, it took like a week to boot. So especially back in the olden days, when things were slow and life was easy, operating system designers made this conscious choice to sacrifice, I would argue, certain safety properties or safety guarantees in the name of speed. So we said, what's the use of a provably correct operating system that's 10 times slower? I'll take the speed and the blue screens. Thank you very much. Now the interesting thing is that we're actually started to see some research that's trying to kind of reverse that. It says, what if I could have a language where the compiler and other portions of the language would not let you write code that wasn't thread safe? And then you can make it fast. So what would we do right now? We write code that's fast, and then we try to make it correct. Maybe we should be doing the opposite. Maybe we should write code that's correct, and then you can work on having your programming effort going towards making that correct code faster. So this is an interesting approach. All right, so a critical section, we define a critical section as a series of instructions, and only one thread can be executing at a given time. The critical sections are typically used to implement some degree of atomicity, to implement atomic actions. So because no other threads can enter the critical section while one thread is inside of it, we can use the critical section again to create atomicity. So the things that are inside the critical section will frequently look atomic to other threads that are executing outside of it. This is an important thing to recognize here, which is that none of these things are magic. So for example, I may use a critical section in one part of my code to protect some shared state. If my partner for this class, or some other idiot, programs some thing in another function that does not use that critical section and accesses the shared state, then there's no guarantees. So the things we talk about, these are tools. They still have to be used correctly. If I forget to lock a certain data structure in a piece of code, then I don't have a lock. And someone else might have a lock, and their code might work fine. So all these things are really modulo intelligent use. But let's figure out how to use them. I mean, that's true for most things. Let's figure out how to use this to implement our previous example. So when you start to look at trying to protect pieces of code like this, frequently there's a methodology approach to it. And one approach is to say, OK, what is the local state? So what state is private to each thread? For this example, what would I consider to be sort of private thread state? What's that? Yeah, so this local variable is not shared between threads. Private state within threads is normally state that you don't have to worry about synchronizing. Shared state, global state is state that we have to be careful. So this is private state. What is the shared state that is being modified or accessed by this function? Simon. Yeah, this global account variable. I'm using some functions to modify it, but I'm assuming that this makes some modification to that amount. And then, more importantly, what lines of this function have to be executed atomically? What lines would be in my critical section? If I had a mechanism for establishing a critical section, where would I start it and where would I stop it in this particular example? I hear line number three. Jeremy. Yeah, so which lines of that? Yeah, so I would start it at line one. I should have had a better animation for this. I would start it at line one, and I would end it at line three. So this is my critical section here. To some degree, and I've written these silly helper functions here, but to some degree, I could have done this by just modifying the balance directly. I'm assuming that there's some account t type here or whatever. But let's say I just had some global variable balance. These are the lines that modify that global variable. And they do it by copying it into a local variable, doing the modification there. But it doesn't map that. Even if I did this, even if I just operated somehow on the account balance directly, this is still my critical section. And this is a frequent, this is actually a pretty frequent design pattern you'll see. People will take some copy of, because one of the things this allows me to do is to use some other type of synchronization patterns where I make some changes to a local copy, and then I try to write that local copy back. And frequently I have primitives that allow me to say, if the thing I write back hasn't changed, since I modified it, then it's OK. So frequently what I do is I copy it locally, and then make some changes, and then write it back. OK, so why is it line 4 in the critical section? This is a good question, yeah, correct. Yeah, so I mean, this is a good point, though. So you could argue that maybe line 4 should be in there. But I would argue that line 4 is not necessary for correctness. So what will happen if I don't put line 4 in the critical section, yeah, suit it. It can give you a local copy, which could be 3,000. I might get a stale update. So what I do know is that this will notify me of the amount that was in my balance at some time. But I don't know if it's the amount now. So what could happen in our previous examples, I might get two updates that would be out of order. One update would tell me I had 3,000. The other update would tell me I had 4,000. And I might get the one that says I have 4,000 first. And I would have to go check my balance to make sure things were OK. So this is a good question, Tim. Yeah, so I mean, yeah, so there's usually a lag in some ordering issues here. And frequently, there's inconsistency with respect to how you would view your account balance. But the reality of the account balance, hopefully, is still established. And somewhere, what I want is that the computer knows what my balance is. It might be bad about telling me what it is, but I want the computer to know. But that's a good question. So you could argue that maybe this function should have some synchronization building as well. So when we design primitives to establish critical sections, there's three things that we're concerned about. The first one is our most basic property. We want to be able to keep other threads out of the critical section when a thread is inside. This is called mutual exclusion. This is the most important property. The other two, though, are equally important for working synchronization systems. The first one is progress. I mean, if I have a bunch of threads that are trying to enter the critical section, eventually, I need to allow them to continue to execute. On server systems and other high-performance computing systems, frequently, you have what the people who work on this will call very hot code paths. Small pieces of code that are frequently in a web server or a database server or an operating system that are being executed over and over and over and over again. A lot of times, those pieces of code are critical sections or have some synchronization associated with them for safety. And if they're too big, and the code inside of them is too slow, then the whole system will slow down. So frequently, these performance optimizers will spend a month looking at these particular pieces of code like at the assembly level. And they'll be like, aha, this one instruction we can lift out of the critical section. So there's one cycle that we've taken off this really, really hot code path. And then they'll order a case of beer to take a week off. Because as we'll find out later in the class, that one cycle actually might improve performance dramatic, especially if this code path is being executed over and over and over again. And so this is the performance issue. We want to keep these critical sections small. Again, my suggestion is that you kind of ignore this last piece when programming your kernel for this semester. When you get hired by Microsoft to improve the performance of their applications, then you can worry about this. When you're writing code for this class, I would put things in the critical section of IREAD. Not everything, because then you're going to have some other issues. But I wouldn't worry too much about scratching my head. If you think it might be in the critical section, just put it in there. You'll be happy you did. So there's two ways to implement critical sections. There's two ways to get them to work. And again, so this design pattern, I just want to preface, is broken on multi-core systems. But on unicorps systems, what you used to see is this idea that one way of preventing other threads from executing a code block is simply, as soon as one thread gets inside of it, don't stop running. Don't let that thread be descheduled. So as soon as I get inside the critical section, and these critical sections are inside the kernel, so the kernel says, I'm turning off interrupts, and I am not going to allow anyone to interrupt me until I am finished. And on earlier versions of OS 161, this was the main way that you established a critical section, which is just shut off interrupts. No one's stopping me. And so by definition, I'm going to run until I get outside of that critical section and then enable interrupts again. Because the timer won't fire. I won't be descheduled. I'm going to run until I'm done. Is there a question on it? Yeah. Well, the timer will keep on firing. But we're masking all interrupts. So we're saying to the timer, I'm not going to handle you right now. As soon as I lift the interrupt mask, if the timer has fired, I may jump immediately into the schedule. But while I'm running inside the critical section, I make sure that the time, I ignore all interrupts, including the timer and everything else. Yeah. You're in trouble. Yeah. So yeah, I should point this out. This type of critical section and what we're really talking about for the next week or two is kernel code. This is not something that you would do in user space because user space wouldn't be able to turn off interrupts. So this is, how does the kernel establish a critical section? If the kernel hits an exception, really, no matter where it is, you're going to blow up and die. You guys will see this message many, many times. Panic, I don't think I can handle this. I'll just die now. It's burned into the eyeballs of everybody who's taken this class. You'll see it in your dreams. So again, in the multi-core area, this just isn't safe anymore, because why? What could be happening? Why doesn't this work on a multi-core system? As soon as I get inside the critical section, I shut off all interrupts, and say, no one else is going to stop me. I'm going to run until I get out of here. I'm going to run until I do, excuse me, all the instructions inside this critical section, and this doesn't work on a multi-core system. Why? Somebody knew. Kevin. The question is, on a multi-core system, why can't I just turn off interrupts to enable a critical section? Does it know? Yeah, so. So the interrupt mask is normally per processor. That's a good question, right? So the interrupt mask is, when I disable interrupts, I'm normally disabling them on my processor alone. Yeah, Nathan. Yeah, that critical section, another thread might be about to enter the critical section on another core, right? So I've shut up interrupts on my core, but even if I was able to turn off interrupts on the other core, there might be a thread who's about to enter that critical section. There might be another thread inside the critical section, right? This just doesn't keep other threads out. This is, so again, this is not a mechanism for keeping other threads out. It's just a mechanism for allowing me to get through before anyone else can do anything, right? This doesn't stop other threads from entering the critical section. It just stops anything else from happening, right? On a multi-core system, I might have four threads that are about to enter a critical section on every core. They all turn off interrupts. They all run through it identically together and I have four threads inside it, right? Exactly what I didn't want to happen. What's that? You still have those scheduling problems. Which scheduling problem were you talking about? Yeah, yeah, yeah, no, no, absolutely. All I'm saying is on multi-cores, the idea of not stopping doesn't work, right? Because I can't, because again, not stopping is not preventing other people from getting into the critical section. And on other cores, I have n minus one other cores where somebody could be about to enter the critical section. So I need to do something strong, right? Okay. So, a thread is inside the critical section or about to enter, again, most generally, and again, I wouldn't even, again, this is just really for historical purposes. I should probably just take it out, right? But how do I do this, right? I need a way to force other threads potentially run in another cores and not enter the critical section while I'm inside. So, this is another case where I am going to rely on hardware to help me, right? This is particularly important when I start looking at multiple cores, right? Which is the whole problem with the example that we just saw, right? Which is turning off interrupts doesn't work. What do I do? Usually what I do is I rely on these special hardware instructions, right? These are, so this is, let me show you some C mockups of these two patterns for how to do this, right? This is not, this C pseudocode is intended to give you an idea of what happens. This is clearly not how these are implemented, right? These are hardwired into the hardware itself. Yeah, get a question. Yeah, so, I don't want to talk about cache coherence too much, right? But usually, each core will have its own local cache, and there are mechanisms for ensuring that that cache is kept coherent with main memory, right? For the purposes of talking about these, you can think about all cores sharing, you know, these are shared memory multi-core machines, right? So they all are sharing some main memory. Caches are designed to make that memory look faster to each core, but they're also implemented in a way that makes sure that the memory is consistent. Keep this memory, yeah, absolutely, yeah. So there are two, let's do this and then we'll be done for today, right? So there are two different ways to do this, right? The first is what's called an atomic test and set instruction, right? Test and set, what it does is it writes a location to memory and it returns whatever value was in that location when the write took place. These are instructions where, as you can see, if you look at the C-suitico, there are multiple things that are happening, but the idea is that the hardware guarantees that these will happen atomically. So if I do a test and set and someone else is running the test and set, the hardware will make sure that those do not interleave, right? If I test and set one, if the value is zero and I test and set one and someone else tests and sets two, one of us will get one and the other one will get two, right? The other one is what's called compare and swap. So what compare and swap does is it says, here, if the value of this memory location is equal to this, set it to this. If not, it's gonna return some, either the old value or some failure, right? So what this says is I give you the value to compare against. If the value of my target is the same as what I've asked you to compare against, then you set it to a new value, otherwise you don't. And so on Wednesday, we'll return to this and we'll look at how to use these primitives to build spin locks. We'll see you on Wednesday.