 Like the inflection, what's that? Are there people that are disagreeing that it's not a good morning? That's a not-too-moron. Ah. That's a not-too-moron. Oh, really? It was a pretty day this morning, actually. It was a nice day. All right, so today we're in synchronization week. We're in the middle of synchronization week. So on Monday we talked about kind of some of the problems, the problems that we face, the problem that we face. We've created this thing called concurrency, and concurrency is like Frankenstein or something. I mean, we've created this thing, and it's out to get us now. It's outgrown its initial purpose, and it's coming back. It's bumping around at night, and we need to find out something to do about it. On Monday we talked a little bit about the hardware bases for synchronization. So hardware support for atomic constructions. And today we're going to continue talking about higher level synchronization primitives. So how do we use those lower level primitives to build things that will actually be useful to system programmers? And in this class we focus a little bit on things that are useful to kernel programmers, because for the foreseeable future that's what you guys are. So we're going to talk today specifically. I'm going to introduce you. This is, I thought a little bit for days I agonized about how to present this. And what I decided to do is do this. So today I'm going to introduce these primitives to you. And unfortunately the best way to introduce these primitives is frequently to use examples, but I'm not going to use any examples. Because on Friday we're going to go through, oh we're just going to spend the entire class going through a set of examples showing how to use these primitives to solve a set of problems. So I don't know if this is the best way to do it. It's how I decided to do it. So if it turns out badly you can blame me and take it out on me by doing badly on the midterm or something. So today we're going to talk about these primitives. And then again, on Friday we will go through several well worked through examples of how to use them in actual code. And you guys will have more chance to use these on assignment one and then throughout the course to solve some real synchronization problems. So for assignment one we're going to give you some toy synchronization problems to solve. So speaking of assignment one, assignment one is on its way. As you guys remember, from waiting from assignment zero, sometimes maybe a little pieces of it will be dribbled out. But it's coming. And part of what we're doing, you guys might say, well, blah, blah, blah, this course has been taught before and all the assignments are online. So why is this so difficult? Why is it so hard? I mean, just take the assignment, just put it up online for crying out loud. Well, one of the things about this class, at least the assignments that I'm familiar with, is that they were usually graded by about 10 times as many TAs as I have for this class. So one of the things we're doing is we're releasing the assignments is we're trying to think about how can we structure them and how can we structure your submissions so that they can be graded effectively by a small group of people. And so please bear with us. And I just want to make sure that when stuff comes out, then it's in the form that I wanted. It's the form you guys are going to do it. So I think we've made a decision about the Tuesday recitation. And the decision about the Tuesday recitation is that there will not be a Tuesday recitation any longer. Sonali showed up yesterday. Nobody else did. This is clearly not a recitation that is super popular. So let's just kill it. And you guys can attend one of the other three recitations. For now, I think those are the time and place for those are set. If we have any luck in moving out of Talbot 111, we'll let you know. But for now, the locations are online. OK, so for assignment zero, yeah. I don't remember which recitation it is. The website is correct. Currently, there are two recitations. And again, I should know this. There are two recitations at Davis 113. Do you remember? That's the Thursday morning. And which is the other one that's what's that? Anyway, you guys could check it on. Apparently, nobody on the staff knows when these are. But yeah, so the website is right as far as I know. Currently, there are two recitations in Davis, and there's one in Talbot. And the Talbot one, again, we might think about moving just because that room doesn't have a projector. But I mean, Sonali's going to get in good shape from logging the projector back and forth to Talbot if we don't find a new room. So I think that's what we've got, OK. All right, so for assignment zero, I kind of see this pattern developing where suddenly, like on Sunday, there's a lot of interest in doing the assignment. So let me point out a couple things to you guys. The first thing is, in general, and the last couple weeks have been an exception, but in general, I don't work after 6 PM and I don't work on the weekends. I work my butt off when I'm here during the day. But I don't feel obligated to answer your emails after hours or on the weekends. So when you guys have due dates, keep that in mind. Because in the future, if you guys are sending frantic emails on Sunday, they're just going to be ignored. I'm actually giving you guys a lot of time to do these assignments, more time than is given at other places. And there's really no excuse for waiting until the last minute. And especially, there's no excuse for waiting until the last minute and sending these completely clueless emails that are like, oh, how do I do this? And that is like, we've covered all this weeks ago. And an email has been sent out repeatedly to the class list. So some of you guys have experienced a little bit more of my brusque sort of trying to clear out my inbox style. And some of you guys, I've been a little bit more beneficent too. So the entire class got a break on the assignment zero deadline, but that's not going to happen again. And look, it's just your responsibility to deal with mysterious internet connectivity issues and bizarre weather events or whatever. I mean, that's why we give you more than enough time to hand these things in. So in the future, this is not going to be a conversation about due dates. So you guys are responsible for getting started early enough to finish. Any questions about that? So OK, here's my little quiz. Full credit for anything. And again, I mean, on assignment zero, if you would submit a half the code reading questions before 8 PM and then the other half after 8 PM, we're going to grade the ones that you turned in on time or just not going to grade the others. So this is the policy when it comes to late work. Any questions about this? Hope this is pretty clear. So for submitting script files, what I suggest you do is just open a web browser inside VirtualBox and upload things that way. You can set up shared folders between VirtualBox and your host OS. I do that to move files back and forth. But guess whose responsibility that is? Yours. And there's lots of documentation about to do this. These aren't mysteries. VirtualBox is a pretty well-supported tool. So there is definitely stuff out there. But my suggestion is just use a web browser inside the Virtual Machine. Yeah. There were 21 questions. OK. There were, yes. Starting with zero. Yes, exactly. So there is a different save button for each. So if we press the save button after the deadline, our assignment is submitted after the deadline. This means that our assignment is submitted after the deadline. Right. Yes. So I think the way I set it up for the first assignment is that if you hit save after the deadline, just none of the new stuff would be saved. But anything that you had previously loaded would be kept. Right. So if there's one answer missing and you hit save at 501, then all the other answers will be in the database. Those will be graded. The other one will just not be there. OK. All right. So, oh, more questions. Is there any chance that the next projects will be like a grading rate for breakdown? Yes. Yes, yes, yes. In fact, I was just talking with Robert about that. So my philosophy with grading, and we talked a little bit about this at the beginning of class, but it's good to come back to you now as we start to get into some of the better assignments. So there's two things. One is that I don't have enough TAs to really do a lot of grading by hand. So a lot of what we're going to do to test your assignments is we're going to run tests. We're going to run tests. And we're going to run tests that we're going to provide to you guys. So in general, I don't think grading should be a mysterious thing. My goal is that when you submit an assignment, I've made it possible for you to figure out pretty much how you did on that assignment. So I'm going to give you guys all the tools you need to test your assignments in a way that's essentially analogous to what we're going to do. Now, we may run the test more times. Just bang on the system a little harder. But the tools to test your kernel are there. And the reason for this is that I want you guys to know how well you've done the assignments. You can decide whether or not you want to keep working or stop. I mean, if you work on assignment two for a week and you've got everything working and you run all our stress tests and everything passes, then you're done. You can go have fun. You can build some more stuff if you want to. You could implement pipes. You could do copy and write for fork, whatever. You could do all sorts of fun stuff. But again, my goal is that the grading, like when you get a grade for this class, I don't want it to be a surprise. I don't want you to think, oh, man, I submitted an A assignment and I get a C. And there is grading to some degree. And I don't, this is unfortunate. But with a big class like this, it should work out. Where grading is going to be somewhat relative. So if the entire class does terribly on assignment two, then maybe a terrible submission will look a little bit better to us. But in general, I think that my experience at UB is that there are really some strong programmers here. So I'm expecting to see some really, really good submissions. And we're giving you guys more time in order to complete the assignments. Does that answer your question? So again, my philosophy with grading is no surprises. If you submit something and we have really different results, then you should talk to us. But our goal is to not have that happen. Our goal is to tell you, here's the test that we're going to run on your kernel. Because these are the tests you should be running anyway. I mean, these are the tests that you run to figure out if stuff works. Because other than kind of look at the code really hard and convince yourself, that's how we test systems. We run regression tests. We run unit tests. We run stress tests. And we see what happens. Any other questions? There was one other question here. Yeah, Calvin? With the submission deadlines? Yep. Yep. No, the goal will be, well, that will just be automatic. So if you submit by that, basically, we will figure out, at some point, you're going to tell us, this is the assignment degrade. And if you tell us that before, if you submit something before the deadline, you won't be docked any. You won't be charging any late days if you submit it after the deadline. We'll calculate it in the number of late days. We'll have it on the website so you can figure out how many late days you have left. So again, this is something that we'd like to automate and it's automatable, so why not? Yeah, did you have a question, Malik? OK. All right, any other questions about this stuff? Good, good, good. And again, the website could be updated with some gradient policies and stuff like that. So I'll try to get to that soon. OK. So again, Monday material, questions. At the end, we kind of went into a little bit of turbo mode. Today, I've tried to keep the lecture a little bit lighter so we can go back and talk about stuff. But let's review. But hold on, let me just give everybody a chance. Monday, Monday, Monday, Monday, concurrency, critical sections, hardware support for synchronization, any questions about that material? Questions, questions, questions. OK, all right, it's quiz time. So back here, I'll just worry. Illusion of concurrency is both what and what? It's a good guess. But the illusion of concurrency is to programmers. You're telling me how it works. But I'm asking you, what is that? Powerful and useful. Oh, right, wow, verbatim. It's incredible that he got that because it was actually on a slide on Monday. So I've started to have a way now of doing this. So these slides are very familiar. And the reason is it helps us think about how to structure our applications. And it can hide latencies caused by slow hardware devices. These are some of the reasons that we concur. Because synchronization, when you guys start working on synchronization problems, sometimes you're going to start thinking, man, why do I concur? Why am I doing this to myself? OK, concurrency also causes problems. So what are some of the problems that concurrency causes over there, the red corner? One problem, you want to help them out? Anybody over in this corner of the room? Synchronization, right? So if I don't synchronize, what could happen? Any other guesses from over here? So I could have a race condition, and a race condition results in what? So a race condition would affect correctness. So the two problems with concurrency are, first of all, now I have these threads running around, and I need to figure out how to coordinate activities between those threads. Again, go back to the Cook's analogy. Everybody's working together. Everybody's doing different things. People are trying to build up something to get. So I need to figure out, how do I communicate between them to make sure that everybody's not making the mashed potatoes? And nobody is making the beautiful sous vide steak. And then correctness. So the correctness is a problem where, if I don't access shared state correctly and in a safe way, that shared state might be corrupted. So this is kind of the bank account example that we looked at. The bank account balance was corrupted by interleaved execution by two concurrent threads. And that was caused. So this is a correctness problem. I think we're going to get to animicity. So concurrency is the illusion that anybody, multiple things are happening at a time, and more importantly, more things are happening at a time than I have cores to actually happen things at a time. Atomicity, on the other hand, is the illusion that what? Over here. Only one thing is happening at a time? Well, it's not just one thing. Step right, things are happening at once. You guys are kind of veering around the answer. Yeah, come. Right, right, right. So something that actually requires multiple instructions happens all at once. From the perspective of anybody else observing the world. Now, the thing here is, if I'm a thread and some other thread's running, how do I observe what it's doing? How does it change my world? Anybody? Any ideas? Remember, what do threads share in general? In address space, there's a lot of shared state between threads. That's what allows threads to work on the same problems. If threads were completely isolated from each other, then there would be no way for them to coordinate it all, and they couldn't be doing the same thing. It's like taking two cooks and putting them in completely separate kitchens and telling them to prepare a meal together. They can't do it. There's no way for them to even. So shared state is the thing that I have to update atomically. So atomicity, so what it means to happen all at once is that another thread is never able to see the shared state in some sort of intermediate or unsafe state. Questions about this? I want to make sure you guys see it. Some threads work depending on other threads work. Sometimes maybe just like a cook that you need to read the recipe first, and then you add some ingredients, just like this, and there are some orders. So how to deal with the concurrency in this case? Right, so I think what you're talking about is an example of coordination. And we're going to talk a little bit. So last time in class we focused on correctness. Today we're going to talk about a couple of primitives that allow us to actually do coordination. So coordination is more this idea of signaling. Like I want to tell another cook, like for example I might want to tell just one cook in the kitchen to do something. And if I just yell it out, three cooks might start doing it. So there are some mechanisms that allow us to signal in certain well-defined ways. So that's a good point. We need to coordinate, not just be correct. OK, so assumptions you can no longer make about your code when you are writing a multi-threaded application, and when you are writing the kernel for this class. What can you not assume about your code? Threads may execute in any order. Be run in any order. You cannot make any assumptions about what the scheduler does. The threads may be run ABC, they may run CBA. You have no idea. So any modifications to make to shared state that depend on thread ordering are going to fail unless you ensure that you get the ordering you want. What else can you not assume about threads? Over here. Could be stopped at any time. Unless explicitly synchronized. And we talked about critical sections, which is a way of making sure that I'm not stopped, or at least I'm not interrupted in the process of doing something. But in general, yes. I can be stopped and started at any time. I don't have control over when I preempt. And then the last thing. The comment, even if you had the power to control the top and start. Right, right, right. So I'm not saying that relaxing any of these assumptions would be sufficient to solve our problems. I'm just saying, these are the things that you have to keep thinking about when you write multi-threaded applications. When you look line by line by line. When you look at two different functions that actually are shared state, you have to think, these functions can be interleaved in essentially any order. They can be run one after each other. That's usually OK. Because that's usually what you want if you haven't synchronized things properly. But they can also be just interleaved in any way. Any way. So if there's any point at which state has been left by a thread in an inconsistent position. And another thread. That is the moment when we grade your assignment where the other thread will run and do something wrong. OK, last thing. This is kind of related to number two. Anybody remember from last time? What's that? Well, that's kind of a function of these three things. But the last thing is, a thread can be stopped for an arbitrary length of time. So that really comes into the interleaving. So again, one way to think about is, if you have two threads, if you have a function that acts as a shared state, that function may interfere with itself and may interfere with other functions that acts as a shared state. And essentially, there's no limit to the number of different ways that you can interleave those unless you do something about it. Yeah, well, the system is going to schedule the threads. We're going to talk about that next week. Well, my point is that you can't make any assumptions about how the scheduler works. You can't assume that, what's that? No. Again, don't make any assumptions about how threads are scheduled. It doesn't matter if you think you understand the system scheduler, because there's probably two things wrong with that. One is that you actually don't. And the other is that it might change. So somebody might write a new scheduler, somebody might change a scheduling policy, whatever. Schedulers in general are very dynamic and fairly complicated in terms of how they work. And so the safest thing to do and the only correct thing to do is just not make any assumptions about that. It's really hard to write code that makes assumptions about thread scheduling anyway. This is just something to keep in mind. OK, so we talked about critical sections. A critical section is an area of code where we think of only one thread being able to execute in that critical section at a time. So it's a stream of instructions. Then once I start executing those instructions, no other threads may be executing instructions that is inside that area of code. The area of code can be a single function. It can be parts of functions. It can be parts of multiple functions. There's no real limit on what I can include in a critical section. Sometimes we think about it. It was only being in one function. But there's really no reason that it can't span multiple functions, and frequently it will. OK, requirements for the critical section. First one is mutual exclusion. What does that mean? What's that? Only one thread can run. It's just what I said. It's the definition. If you don't get that right, then you don't have a critical section. But one of the things, and I should have had a slide about this, one of the things to keep in mind here is, and maybe this goes without saying, but none of the tools that we talk about today work if you don't use them properly. So for example, you may set up a critical section in your function that controls access within that function to a shared variable foo. Unbeknownst to you, your partner for assignment 2 may write another function somewhere else that modifies foo without grabbing the same lock or without joining your critical section. Your toast, it's over. So getting this right is important. These primitives are tools. If they're not used correctly, all bets are off. And a lot of what you guys are going to struggle with is getting this stuff right. So these tools are useful. They're powerful. But if you don't lock around every access to a shared variable, then you might as well not lock anywhere. So mutual exclusion. Progress. What do I mean by progress? Anyone remember? At some point, if I try to enter a critical section, I'm going to enter the critical section. I'm not going to be stopped forever. And usually, I want to bound the amount of time that threads spend waiting to get in a critical section. Now, that could vary based on the number of threads that are trying to get in, because I have to lonely about one at a time. But in general, I don't want a situation where, for whatever reason, one thread's been trying to get in for many, many, many time quanta, and other threads keep jumping in front of it. So this is attached to an idea of fairness. If I try to enter the critical section, I should be able to execute in the critical section. And we'll talk a little bit on Friday about how scheduling can actually impact this guarantee. So that's an interesting subject called priority inversion. OK, performance. What's the last thing about critical sections? I want them to be small. And the reason is, when I'm executing inside a critical section, I'm essentially reducing the amount of possible concurrency on the system. So if I make my critical sections really big, I'm taking all this useful concurrency that I want to all this trouble with context switching, and multiple cores, and all this stuff to create. And I'm essentially reducing it down to nothing. I don't want to do that. I want the system to be as concurrent as possible while being safe. So I limit my critical sections to only the instructions necessary to make changes to shared state. And there are some interesting design patterns. We'll talk about one on Friday, where one way of, let's say, I have a bunch of complex updates to do to a piece of shared state. One of the ways to do that is to create an object locally, set it up the way I want, and then use a critical section to just swap the object with the object that's already there. So rather than having all the modifications I need to make inside the critical section, I make my modifications locally first. And then, I quickly copy my object or flip a pointer around so that my object is included in the shared data structure that I'm accessing. So that way, I can reduce the critical section down from all these instructions that I needed to set up whatever new leaf in my tree I was setting up or whatever. And I can reduce it down to just the pointer swap or just adding a pointer or something like that. OK. So let's see. OK, I think we've basically done this. OK, any other questions about concurrency and critical sections in particular? All right. So at the very end of Monday, we were going through this banking sample, and we're going to go back to the banking sample in a minute just in case anyone needs any more hints about what I'm lacking in my own life. But this is, so what we came up with at the end was an example of using what's called a lock. A lock is a very, very common and very basic synchronization primitive that are used to implement critical sections. And the way I think about it is, imagine there's a room and you have some shared state, and that shared state lives inside the room. And the way you access the room is you go inside and you turn around and you lock the door. And while the door is locked, you can make whatever modifications you want to that shared state and no one else can see it. When you leave the critical section and open the door, now that shared state is visible to anybody else. Now, of course, the problem with this analogy is, as I said before, if you have shared state that is accessed both while holding a lock and while not holding a lock, is it inside the room or outside the room? So now we have a quantum mechanical problem. So this is not a perfect metaphor. But the point is that this is one way of thinking about this. Mutual exclusion implemented using a lock. So let's modify our example to use a lock. And what were the steps that we had to go through to fix the synchronization problem with this give-goi-the-muloff function? What was the first thing we had to do? Whoops, there it is. Identify the critical section. So who remembers what the critical section was? No, Malik, you're answering too many questions. I need someone who hasn't spoken up today. What about you? One to three, right? So unfortunately, I forgot the line numbers today, but it's essentially this piece of code, OK? And then once I identify the critical section, using a locks is quite easy. What I need to do is I need to lock before I enter the critical section. I need to unlock after. So here's my critical section. Using the terminology associated with some of the primitives that we're going to ask you to implement for assignment one, first I need a lock. I need some sort of shared structure that holds information about the lock. And again, you guys are going to implement this. And then I need to acquire it and release it at the right points in the code. Questions about this? Yeah? Right, so lock acquire and lock release are right. The implementations of those have to be atomic. And the way they work on your system is that they, or the way they, well, I shouldn't say this, normally locks are implemented using a lower level synchronization. Your system has some lower level primitives that can be used to do this. But at some level, what ends up happening with any of these primitives is that you start with some C code and maybe you update some state. But eventually, there's some hardware instruction that you're going to use that is guaranteed by the system to be atomic. So all of these things are based on those hardware basis of atomic atomicity that we talked about last time. So the test and set instruction, compare and swap. Some hardware instruction that the hardware guarantees to be atomic. Calvin, yeah? Yep. So on a multi-core system, what the hardware has to do is it's still, like test and set, for example, has to work on a multi-core system. And multi-core systems go to great lengths to get those instructions to work properly. So a multi-core system will say, test and set has a certain semantics where it's guaranteed to be correct across all cores. And you can imagine that might require flushing cache lines from other cores. That might require locking the whole memory bus so they don't have interleaved access. So there's a fair amount of effort that goes in at the hardware level to guarantee an atomicity. But those hardware instructions, even on multi-core systems, have to be atomic. Otherwise, there's no basis for synchronization, at least not in an easy way. So this is how we use a lock. Now, last time we talked about a particular approach to this. But let's say that what I want to happen, ideally, is that I want lock acquirer to just return. No one else is in the critical section. I'm clear. I release the lock. There's no concurrency problem. And if the calls don't overleave, then this should just run the way we would expect. The problem is what happens if a thread calls lock acquirer, and there is another thread inside the critical section. So what do I have to do? Well, there are two approaches. So last time we talked about active waiting. So I have to wait. I have to wait somehow. I cannot execute the instructions inside the critical section until the other thread exits the critical section. So I need to do something in the meantime. And there's two approaches to this. How do we wait? So last time we talked about busy waiting, spinning. So we talked about a spin lock implementation where I repeatedly ran this test and set instruction until I got a result that indicated that I could proceed. And this is one approach to doing this. Now, what's a, and people had hinted at this last time, what's potentially a better approach or different approach, I should say? I can passive wait. I can sleep. And sleeping is done in the kernel, on your kernel and in other kernels, by telling the kernel, I'm going to go to sleep. I would like to be descheduled. However, I would like to be reawakened. So I'm going to go from the running state to the waiting state. But I'm going to tell the kernel, this is some information about what happens when I want you to wake me up. So frequently, you can think, in OS 161, David has implemented something called wait channels. That's one way of thinking about it. I'm waiting. It's like a queue. I get into a particular queue, and I count on the guy who's inside the room to poke me when he's done. But I need to give something to the kernel to identify when I'm waiting on. And that's done in different ways. There can just be some arbitrary key. We'll talk in a second about how this would be implemented. But the idea is that the kernel has to provide a way for me to ask to go to sleep and to give it some information about what is going to happen in the future that is going to cause me to wake up. Now, last time I may have given you the impression that spinning, active waiting, is never a good idea. And it turns out that this is not actually true. And there's a reason why, for example, your system has spin locks, and many other systems have spin locks. Now, spin locks are almost, I shouldn't say almost, are probably never, ever a good idea on a single core system. Why is that? Why would I never want to spin waiting for a lock on a single core system? Yeah, Ben. And what other thread might use those? Exactly. So if I'm waiting on another thread to execute the critical section on a single core system and I'm burning up CPU cycles, active waiting, then I'm guaranteeing that that thread is going to take longer to finish. So the best thing to do is to get out of the way as fast as possible, go to sleep, let the thread run, and have it wake me up when I'm done. So in general, spinning on single core systems is almost never a good idea. I can't think of a case in which is a good idea. Maybe I'll look it up and see if I can find one. OK, now on a multi-core system, why is this different? Why does this assumption relax on a multi-core system? Anybody want to venture a guess? So I think that's right, and let me explain it a different way. On a multi-core system, it is possible that the thread that I am waiting on is running on another core. And in that case, what happens is, so on a single core system, nothing can change. If the critical section is short, really short, on a multi-core system, it can be beneficial to actually wait actively. The reason for this is that context switching, which is what I'm going to do if I go to sleep, has overhead to it. We talked about this a week ago. All these registers I have to save, and I have to reschedule another thread, et cetera, et cetera. So sleeping is not free. Sleeping is going to take some time. And then I have to rely that at some point later in the future, I'm going to get a chance to run. So if what I'm trying to do, and frequently when I have a critical section, what I'm trying to do is I'm trying to maximize the throughput of threads through that critical section. When threads are inside the critical section, I want them to leave. And I want the other guy to get in as fast as possible, assuming there's a lot of contention for the critical section. So here's an example of how this might work. If my critical section is really short, so here's a case on a two CPU system where thread one was inside the critical section, but the critical section is very short. So thread one only spends a tiny, tiny little bit of time. Sorry. Oh my gosh, I've got two thread ones. OK, never mind. The green thread. The green thread is inside the critical section. The red thread over here tried to end their critical section and failed and decided to go to sleep. And what happens? So there's all this overhead to doing a contact switch, and then another thread is going to run, and then I have another contact switch. And finally, down here, this guy gets to run again, and he can enter the critical section. But what's happened is I've wasted all this time right here where the thread, this thread, had it just busy weighted a little bit, could have entered the critical section more rapidly. So I've reduced the throughput of the threads through my critical section. On the other hand, if the critical section is long and I spin, then I'm wasting CPU cycles on the other CPU. So here's an example. Let me just finish this one, where this guy's inside the critical section for a while, and this guy should probably go to sleep. He doesn't. He decides to spin, and he just whack, whack, whack, whack, whack, whack, whack, whack, and then finally, later, he gets it. So this is a period of time on CPU 2 that could have been used to do other useful work. Sorry. So you had a third thread? Yeah. Oh, gosh. My brain can only handle two threads. But if you come to office always, we can work through this example with three threads. Well, you as a programmer know that. When you write your code and you guys design critical sections, you will have some concept of how long they are. For this class, your critical sections will almost always be long enough where it makes sense to sleep. But what you'll see is that inside some of the synchronization primitives that you're going to implement, there are cases where the critical section is very short. And in those cases, you actually use a spin law to access some shared resource. So one of the things you guys are going to do for assignment one, you can start this whenever you're ready, is you're going to look at the implementation of semaphores inside your kernel. Because that implementation works. That's the hints that we've given you for the other pieces. We gave you a working implementation of semaphores. You can run the semaphore test that works, and you can see how that code is implemented. And the semaphore implementation, talk about semaphores in two minutes. Semaphore implementation that we gave you uses a spin law. Any other questions about active versus passive weighting? I just want to make sure that you guys understand this design choice. Because it's really, again, it's another one of those cases where there's a trade-off between how I access resources. Yeah. Yeah. And well, actually, I don't know if that's actually implemented on real systems, but you could think about it. You could do some sort of compile time profiling to try to understand how long the critical section is and choose an appropriate synchronization. You also have cases. We talked before about not wanting to spin on single-core systems. So there are things called adaptive mutexes that essentially look like a lock, but will turn off interrupts on a single-core system, which we talked about last time, but will not spin. And then on a multi-core system, they will spin. So you can just use it like it's a lock, but the system decides how to implement it based on the hardware that you're creating a system for. Yeah. Yeah. Yeah. Yep. Frequently, no. You might have some others. So I don't think so. So one of the things I should say is that the audience for this discussion, again, as I said earlier in class, is really kernel programmers. So I'm focusing more in class on the applicability of these primitives to system programming. How these are used in user space, there's actually a fair amount of similarities when you talk about locks and condition variables. And certain languages like Java and lots of other languages have even more sophisticated support for concurrency that tries to make things even easier for programmers. So Java has these synchronized keywords that essentially implement something that's almost like a lock. But for the purposes of this class in trying to keep things simple, I'm going to try to focus on how you would use these to program an operating system. But yes, so maybe to try to weasel around answering your question, there's a lot of good language support for concurrency and for synchronization. And that may try to do adaptive intelligent things. So again, these primitives are typically not provided. So there's no API, for example, for using the low level synchronization primitives that the kernel uses. A lot of these can be implemented in user space and libraries. So if I have a thread, if I have a user space thread package, I can implement locks without entering the kernel at all. Just using my own knowledge of scheduling. Because if I have a user space thread library, I have a user space thread schedule. And so I can do some of these things in user space. So there are ways to implement these primitives that don't involve any kernel support at all. And to the degree to which kernel provides the support for building synchronization primitives to applications is a question that I don't know how to answer. But I will happily look it up for you. Yeah, other questions? All right, so let's talk, all right, so we talked about this before, right? The kernel provides a primitive that allows threads to sleep on some key. That could be a number, it could be whatever. In OS 161, it's a specific thing that you should figure out what it is. But it's usually some sort of identifier that identifies the kernel. This is the thing that is gonna cost me to wake up. And then I can also wake up threads that are waiting on that value. And locks are implemented by, if I look at some piece of shared state, if the critical section is busy, I go to sleep. And when I'm inside the critical section while I'm at release the lock, I call wake. And I allow other threads that are waiting on that critical section to continue. And as I just pointed out, you can implement these, there's a lot of these primitives in user space. All right, so locks are, in many ways, one of the more useful from your perspective of as kernel programmers, the more useful synchronization primitives that we're gonna talk about in this class. But locks are really fundamentally designed to protect critical sections. And as someone pointed out before, I might have the need also to enable certain forms of structured communication between threads. Lock release, you can think about lock releases is implementing some communication pattern. I mean, when I call lock release, I'm essentially telling the threads that are waiting at the critical section, hey, one of you guys can enter the critical section. And when I call lock a choir, I'm essentially saying, hey, I'm inside the critical section, nobody else can come in. I don't know exactly why I put that up on the slide. But let's talk about sort of other kinds of communication between threads that I might want to enable, right? So what about if there's some data buffer that I'm filling and when I'm finished, I need to notify another thread that that buffer is full, that that thread is gonna do something with the buffer, like pass it to user space or processor or whatever, right? Or when we talked about the semantics of fork and exit, I might need to tell the parent process when it's child exits, right? I mean, it need to create a way for the parent to find that out, right? Or to access some shared state in a structured way. So condition variables, you can really think of as a signaling mechanism that allows me to wait until something becomes true or something changes and to notify other threads when that thing changes, okay? And unlike locks, the thing that's changing is more general than just somebody's in the critical section or not in the critical section, right? And so for example, that condition is usually represented as some change to the shared state that me and another thread are accessing. So for example, going back to the buffer that I just talked about, right? I might wanna notify other threads that a buffer has data in it, right? So the shared state is the buffer, right? Or some size of the buffer variable that we're maintaining that's supposed to represent how much data is in the buffer, okay? And the communication is I wanna tell somebody who's gonna use the buffer, hey, the buffer's full and you can continue on and do what you need to do, right? I also want threads to potentially be able to wait until that condition becomes true. So if the thread that's waiting for the buffer doesn't have anything else to do, it might wait on the thread that's filling the buffer to fill it and then when that thread is done, it's gonna signal this guy that he can proceed and do something with the data that's there, right? Again, these are better done with examples and we will do examples on it, right? So I just wanna introduce the semantics so that we can come back to it. Maybe this was a big mistake. And again, so the CV signal is just a way of notifying other threads that, you know, there's data in the buffer now, right? I filled the buffer and some other thread that might be waiting, it might not be waiting, right? I might just change the state and then when the thread shows up to access the buffer and I find data there, I just proceed, okay? So again, condition variables convey more information than I can with the lock, right? All the lock reflects is, is a thread inside the critical section or not? And when I think about my buffer example, I've got three states the buffer can be in, right? It could be full, it could be empty, or it could be somewhere in between and I've got two things that threads are trying to do, right, they may be trying to put in data and they may be trying to withdraw data, right? And depending on the state of the buffer and the kind of thread that shows up, I wanna be able to do different things, right? If the buffer is full, then I can't let threads put new things into it but I can let threads take things out of it if it's empty, vice versa, and if it's neither, then I may be able to try to safely allow threads to both add and remove data from a buffer, right? So we're gonna go through this example specifically on Friday, yeah. So there is a difference between notify and signal and we will come back to it on Friday. It has to do with the number of threads that are awakened. Right, broadcast and actually I'm not sure why I wrote notify, I probably should have been more careful. Usually condition variable has one way of telling one thread that's waiting on the variable and another way of telling every thread, right? And those can be useful in different situations, right? So in certain cases, something changes and I just wanna tell one thread, any thread, any thread that's waiting and in other cases, something changes and I need to tell every thread, right? And we'll talk a little bit about why to use either one of those calls on Friday. Yeah, Robert, yeah? So I'm talking, so for the sake of these examples, we're talking about communication through some shared state, right? Shared memory usually, and specifically, we'll be talking about communication within the kernel, right, so kernel threads communicated with each other through, yeah. Right. Someone else didn't ask for, you wanna notify the kernel, I won't pay, I'm releasing it as free now on that particular variable being an object. So again, I just wanna, but I just wanna be clear, right? There is no, can we turn off our phones before class? Hello? I just wanna be clear that there's no, I'm not sure what the API is for communicating with the kernel, right? For the sake of this, let's talk more about, you know, communicating through shared memory, right? Either in user space where I can implement these or in the kernel, right? All right, and then, how much time do I have? I'm gonna take two extra minutes today, all right? So semaphores, right? Semaphores, how many people are familiar with semaphores? Does anyone use a semaphore? Okay, good. So semaphore you can think of as a shared count. And what I provide are ways to increment and decrement that counter-automatically. Now, the only trick with semaphores that makes them into a synchronization primitive rather than just a reference count or something like that is that the semantics of semaphores is if the value is zero and I try to decrement it, I have to wait until the value becomes greater than zero. So the semaphore value can never fall below zero. The semaphore is zero and I try to reduce its value, I'll wait until somebody else increases it and then I'll be able to decrease it, right? And so there is, now we can also talk about something that's called a binary semaphore. So a binary semaphore only has two values. And on a certain level, a binary semaphore, if it's used in the right way, starts to look a lot like a lock. So here's an example from before where I use the decrement and increment operators on a binary semaphore to implement a lock around the critical section in this function. Does anyone know what the difference is? It's an important difference between a lock and a semaphore, a binary semaphore, right? They look very similar. Yeah, yes, yes, yes, yes. So locks have the concept of ownership. When I call lock release, the lock implementation will generally check to make sure that I am the one who is holding the lock. If I'm not, it'll fail, right? And they usually do something, it'll assert something that will cause the system to fail because that's an error in the programming pattern, right? A lock is like I'm giving you a token, you have to bring it back to me, right? Semaphores have no owner. So even anybody can P and V this semaphore in any order. And that's the difference. Semaphores are a synchronization primitive that I am gonna just put out there for this class are not necessarily all that useful. Semaphores have, part of the reason that semaphores are not that useful is that they can be tricky to get right because they have these loose semantics. So if you use them in the place of locks, you don't get that extra guarantee that says that the person who's holding the lock is the only one who can release it. Because that can be a really helpful programming tool that helps you find bugs in your code, right? But the semaphore, you don't get that. Anybody can P and V this semaphore, anytime they want to. And so locks will help you program your problems. Semaphores do have a place though when it comes to things that are naturally counted. And we'll look at this on Friday when we use semaphores for an example, right? So let me just finish up by saying on Friday we're gonna go through a set of synchronization problems. You're gonna do more on assignment one and you're gonna keep coming back to this throughout the semester because there'll be lots of shared saving that currently have to synchronize. In general, the tools that we're giving you, there's usually a right tool for a problem. And part of what we're gonna try to do next week through Friday and the assignment is give you some intuition about how to choose the right synchronization primitive. In many cases you can solve the problem using multiple synchronization primitives, right? But one of the solutions will be weird and kind of delicate and won't feel right even though it will kind of, it will work. And the other solution will be like, right, that's the right thing. So we're gonna work on this on Friday and then on assignment one.