 To finish up synchronization today, we'll talk a little bit about problems that you might encounter. So there's two types of problems today. We're going to talk about problems that you might encounter when you're trying to use synchronization primitives. We're not going to talk about priority inversion. That's a bug. I got rid of that. We'll talk about it later. We will talk about, and then we will talk about how to use synchronization primitives to solve one or two synchronization problems. So you have a situation where you need to synchronize the behavior of some threads. How do you go about doing that? And we'll solve a bounded buffer producer-consumer problem using a condition variable. And then we'll talk a little bit at the very end about how to approach synchronization problems, and particularly how to choose a primitive that's a good fit for the problem you're trying to solve. So tomorrow, Jing Hao's going to focus on, I think, condition variables, and then also helping you guys think about how to address the synchronization problems that are part of assignment one. As a reminder, the reader writer locks and the whale mating and stoplight problem are hopefully fun exercises for you guys, but they are not code that you will need to rely on when you do later assignments. So if you are looking for a place to stop, or if your locks or condition variables don't work, I would suggest that you focus on those things rather than getting sucked into working on whale mating, which is obviously super fun, who wouldn't want the chance to help whales copulate, but not necessarily something that you guys will need for assignment two. There will be no mating whales on assignment two or three on north stoplights to get through. As a reminder, Jing Hao will be gone on Friday, so we will not hold the A&M recitation. We will post the video as usual tomorrow after recitation, so you guys who might go to the A&M recitation normally can watch it at your leisure. And then Wednesday and Friday, I'll be gone. We're going to show videos, and I will post those videos to the class playlist, probably today or tomorrow. And you can watch them at home in your underwear, or you can put on your clothes, hopefully, and come to class and watch them here. On Wednesday and Friday, it doesn't matter, right? So whatever you feel most comfortable with. All right. Any questions about stuff we talked about on Friday? So we talked about locks. We talked about different strategies for waiting. And finally, we talked a little bit about condition variables, and that's sort of where we'll pick up. Any questions about Friday's material? All right. So let's review spinning versus sleeping. So spinning is almost never a good idea. You might even say categorically never a good idea on what type of computer. Yeah. Yeah, on a single core processor. Of course, again, you'd be hard-pressed to find one of those now, so this may not matter. If I'm trying to decide whether to spin or sleep, I have a choice. I have two different locks. When you're done with assignment one, you'll have two different locks. You'll use them later. What helps me make the decision? Yeah. So if the critical section is very short, what do I want to do? Spin. If the critical section is long, sleep. One addendum to this is that if your thread is going to do anything that's going to block, so when we talk to a guy start doing assignment two and assignment three, your threads will do things that will cause them to be put to sleep by the operating system. And this is something that we'll get back to when we talk about threading. For example, you might initiate some IO to the disc. That IO is very slow, so while that IO is taking place, your thread is going to be put to sleep. That automatically makes the critical section really, really, really, really long. So whenever you do anything that might sleep or might have to block, particularly if you're going to go to disc, you can never use the spin lock, which is a terrible idea. Somebody else will be sitting there, just blocked for quantum after quantum, trying to get through a near, like, waiting for IO to happen, which takes ages, just a long time. OK. Yeah, so that was the overhead of the context switch is what we're trying to trade off here. If it's too short, then the context switch is longer than the critical section. If it's too long, the context switch is short. So it's better to see. OK. What's the interface to the lock? Specifically, what's the interface to the implementation of locks that we have asked you guys to complete? There's two primary functions. What are they? Acquire and release. So acquire will, ah. Somehow this has been here for, like, three years. I'm sorry. Maybe you guys feel like that makes me a terrible professor or something that I just keep forgetting. But maybe I'm just amused by that. Clearly, it's wrong. Release will release the lock. Release will never sleep, right? Release does not sleep. Lock acquire will sleep only if it needs to. These are the locks that we gave you. These locks sleep, right? What do I use the lock for? Primarily, protect a critical section. I have a part of the code. I only want one thread to be in at a time. I can use a lock to do that. Of course, you can use a lock for a lot of other things. I can use a lock to protect multiple critical sections. I might have several different functions that access the same shared state. And I actually need to make sure that there's only one thread executing in any of the functions at a given point of time. Very straightforward application of the same principle. Lock at the top of the function, lock at the bottom, and you're good. So you can use locks in a variety of different ways. All right, condition variable. So condition variable, remember, is a signaling mechanism. It allows threads to wait until something is true, as well as inform other threads about when the condition has changed. CV wait and CV notify, CV notify and CV broadcast don't necessarily understand what's happened to the condition, but they're a way of triggering other threads to wake up and see if what they've been waiting for has happened. And the condition is usually represented by some change to shared state. I'm going to just keep going because we're going to get back to this in detail today. So we go through this example. So condition variables allow you to represent and to inform other threads in ways that expand your vocabulary if you're just thinking about locks. Really, all the lock tells another thread is I'm busy in this critical section, you have to wait. But the condition variable, I can use a condition variable to inform threads about a variety of different things. And again, we'll come back to this when we do our buffers. Yeah, maybe I'll sort of wind back to these when we start doing that and we'll talk about this. So why do we consider, I mean, in that formulation, condition variables seem almost exclusively like a signaling mechanism, like a communication mechanism. Why is there this added element of synchronization that's thrown in? Anybody know? Want to guess? Yeah. Mm-hmm, right. So there's something that we need to make sure is atomic. When I'm using a convention variable, typically what I'm going to do is I'm going to check the condition and then I'm going to wait on the condition variable. And when I am reawakened by somebody who's calling broadcast or signal or notify, I'm going to recheck the condition. So what's the little part in there that I need to make sure is atomic or condition variables to work? There's two parts of that that together require a number of different instructions. But I need to make sure those things happen at once, because if I get stopped or descheduled in the middle of them and another thread can run, then a bad thing can happen. What are those two actions? Yeah. Yeah, so I need to make sure that the condition doesn't change after I check it when I'm on my way to sleep. And we'll come back to this when we look at the producer consumer buffer. But in order for this to be safe, I need to make sure that that condition can't change until I'm asleep. Because if I check the condition and then I get descheduled and somebody changes the condition, then I'm going to sleep. It's too late. And they may have already woken up the people who were sleeping on that condition, but I wasn't one of them yet. So I need to make sure that those two actions checking the condition and getting onto the weight queue or whatever mechanism I'm going to use to sleep happen atama. And here's an example. I checked the buffer. Now, if I was able to be descheduled right here, this guy could put data in the buffer. He could call notify on the buffer, but then I'm already sleeping. Because his actions happen after I check the condition. Yeah. Whoa, what just happened? Somebody is hacking my pointer. OK, cool. So any other questions about this? This is pretty much where we left off last time. Yeah. Yeah. Yeah, so remember, condition variable doesn't actually represent anything about the condition. It's kind of a weird name, right? Signaling, you know, thread safe signaling or synchronized signal or something like that. Maybe that would be a better name for this particular synchronization primitive. The condition variable doesn't know anything about what the condition is. It just allows threads to communicate safely about changes to the condition. Does that answer your question? I'll show you how this works in a minute. So the primitive we didn't talk about on Friday were semaphores, right? Semaphores are the primitive we provide you. It's one of the semaphores are one of the earliest synchronization primitives, I think that was proposed. They were proposed as part of a very, very early operating system design. Semaphores are, you can think of a semaphore as two things, the shared counter. It has atomic decrement and increment operations, which is described. And the idea here is that the value of the semaphore can never go below zero. So if a semaphore is zero and somebody tries to decrement the semaphore, what does that person have to do? They have to wait until that semaphore has been incremented by somebody else, right? So this is a really easy way to think about semaphores. Forget the interface, we'll get there, right? The idea is it's a shared counter. There are atomic increments and decrements. And their counter protects itself from going below zero by forcing a thread that would cause it to go below zero to wait, right? The thread, the semaphore interface is V signal or up, which is increment the value, and then P, which is wait or down, which if the value is zero, decrements it. If the value is less than zero, it has to wait until the value becomes greater than zero to decrement, right? So this is up and down. And just like locks, right? So this looks a little bit like a lock, right? What is V like? Lock a choir or lock release? Release, right? V will never sleep, just like release. V cannot sleep. It will always succeed. P is like lock a choir, right? It will sleep if the value is about to go below zero, otherwise it won't. It'll just proceed on through, okay? So these names are kind of funny, right? So I always like, you know, when I was your age and someone told me about these, that person who's very venerable, individual, very trustworthy had told me, oh, these are Dutch words. And I was young and I didn't really care what they meant. So I was like, that's fine. And then when I taught this class, I was like, I should find out what they actually mean because now I have to know this stuff. And so V stands for verhogen, which means increase. P is the one that's a little bit more interesting because if you go to Wikipedia, there's a lot of theories about what P actually means, including like made up words that aren't actually even Dutch, right? So I kind of feel like I was lied to when I was younger. No one really knows what P stands for. You would think someone would have just decided what it meant, right? Like the guy who invented them. Why are these words in Dutch? Who's the famous Dutch computer scientist that everybody knows about? Yeah, Edgar Dijkstra, right? So that's the rule. If you create it, you get to name it even if you name it after silly made up words, right? You're allowed to do that now, it's your thing, right? So the rest of us, the generations of computer systems programmers have been forced to learn P and V, right? And I actually forget what they mean on a regular basis, right? So again, so a binary semaphore is a semaphore that's initialized to be either zero or one and can be restricted to only have those values. And usually the way you do this is you just set up your thread so that the value never goes above one. So how would I use a binary semaphore to look like a lock? What do I initialize the value to? One. So that ensures that the first time the semaphore is, again, there we go, p'd, it doesn't block, now it's zero. So now let's say there's a thread that's inside the critical section and I'm protecting using the semaphore. What happens in the next thread that tries to p that semaphore? It'll wait, right? Until the first thread gets to the bottom of the critical section, hits the V, value goes up and the other guy goes through, right? So that's a way to use a semaphore like a lock. So there's the example, right? So we could rewrite our wallet example to use P and B. Now, but here's the interesting thing. What is the difference? You guys have probably already know this because you guys are probably almost done with assignment one, hopefully, at least with locks. What's the difference between a semaphore, a binary semaphore and a lock? Yeah. Yeah, so a lock has a concept of ownership, which a semaphore lacks. If one thread calls lock a choir on a lock and another thread tries to call lock release on that same lock, what should happen? Like that's wrong, right? Locks require that you pair a choir and release, right? That's part of the lock semantics. That's part of the interface and you should enforce that when threads call release, right? Because it will help you down the line to ensure that you're using locks correctly. Semaphores, they don't care, right? If you use somebody who calls P, anybody else can call V, right? So semaphores lack a concept of ownership and in some cases that makes them useful. Okay, so we'll come back at the very end, talk about semaphores a bit more. Let's talk about it. Yeah, so account, so I think it's useful to split this question up into two parts, right? So accounting semaphore, right? A semaphore that's allowed to have values that are other than zero, one can be used in certain cases. Let's say you want to know that only six threads are ever doing a particular thing, right? I don't know why. There are actually, there are some cases in the test driver code where we use semaphores in a useful way, right? But let's say that I want to ensure that only six threads are ever executing in one particular function, right? Maybe the function has really high overhead or something like that, I don't know, right? So each one of those threads grabs the semaphore. I initialize the semaphore to six, right? They start grabbing it as they go through, six of them will get in, and then the rest will block. And that semaphore will effectively ensure that only six can ever be executing that one function. So in that case, there's not really a concept of ownership, right? There's multiple threads that are working on the same thing at the same time, right? With the binary semaphore, you could include ownership in a binary semaphore, but then what you'd have is a lock, right? And there are cases where a binary semaphore can implement a useful signaling mechanism. It's called a barrier, and I have an example of that later on the slide, right? So let's come back to that. I'll show you a case where it can actually be useful for a thread to P, a semaphore, din V, and vice versa, right? So there's, and this is the one time in your OS 161 code base, there's one place, it's like they're waiting for you. If you choose to find it, where you can use a semaphore in a useful way during assignment too. You don't have to, but I think some people just get so excited by the idea of being able to use a semaphore, they're like, awesome, I'm gonna definitely do it that way, right? And it works. Okay, good question. Any other questions before we talk about problems? So now we're into the problems with synchronization primitives part of today's class. So one of the things, so here are some of the pit, one of the pitfalls you can run into, particularly when you start to use locks, right? You think locks are awesome. I'm gonna start using locks. I'm gonna use locks to protect a whole bunch of stuff. And then you run your system and it gets halfway through printing the menu and it just sits there, right? Frozen. And we can show you if you guys come to office hours how to use some of the tools that come with OS 161 to actually verify that your system is broken in this particular way. But usually what's happened is that at a number of points during execution, a thread may require exclusive access to more than one resource at the same time. If those resources are protected by locks, that usually involves what? What do I need to do? Let's say I have two resources that are protected by locks and I need to use them at the same time. What do I have to do? I don't necessarily have to wait. Need to acquire both locks, right? Got R1, R2, I wanna grab the lock for R1. I'm gonna do some work with R1 but then in the middle of it I need R2 as well so I'm gonna grab the lock for R2. Now I safely have access to both resources and I can go about my business. But let's think about a case where this can go wrong. So thread A starts running, it grabs the lock for resource one. So thread A is starting a process where it needs both resource one and resource two. Now here's the problem. Thread A gets stopped. It's time for a new thread to run. Thread B starts to run. Thread B grabs the lock for resource two. So far no problem. This is okay. Except for the fact that when thread A starts to run again, like I said, it needs both locks for both resources. So it's gonna try to acquire the lock for resource two. What's gonna happen to thread A? Oh, that's, you guys are getting ahead. What will happen to thread A? When thread A tries to acquire the lock for resource two, it will what? Sleep, right? It's gonna be put to sleep. And then, this is the last element of our tragedy. Thread B wakes up. Thread B also needs resource one and resource two. Thread B tries to get the lock for resource one. What happens to thread B? Also sleeps. And now, we're stuck. Now we have two threads that are both sleeping. And the reason why thread A is sleeping is because it's waiting for thread B to make forward progress. And the reason why thread B is sleeping is it's waiting for thread A to make forward progress. So we have a cycle, and this is a condition that's referred to as deadlock. So deadlock occurs when a thread or a set of threads are waiting for each other to finish. And by waiting, they prevent each other from making any forward progress. And thus, nobody finishes, and the system will effectively hang forever. This is usually just a completely fatal condition and results in you pressing control C and killing system 161, or if you had a real machine, you'd hit the reboot button. So in an interesting case, a single thread can actually deadlock. How can this happen? Let me give you an example of a case where a single thread can deadlock. Yeah, what's that? Is your answer a recursion? Yeah, okay. So don't write a recursive loop that grabs a lock. Yeah, that's a terrible idea. Don't write a recursive. I have a suggestion about how to complete the assignments this semester. Don't use recursion. If you find yourself using recursion, come talk to us. Recursion is a really cool technique to use in user space. When you have a huge stack that can grow without bounds and the kernel with the teeny weeny little stack, don't recurse, you'll get into trouble. But that could happen, that's a good point. What's another case? Yeah, you had an answer in the back. Yeah, so if a thread has a lock and then tries to relock the same resource, okay? Now, this sounds totally ridiculous. Like, why would this ever happen? This just sounds like a complete bug, you know? Like something that you should have, you know, you're sitting there squinting at your code and it's like, whoops, yeah, I got lock acquirer, lock foo. And then two lines of code and then lock acquirer, lock foo. Obviously that's gonna be a problem. I'll just delete one line and I move on, right? Is that always the case? So I'm gonna give me an example of a case where this might happen and it might not, might not totally, totally be as simple as just, you know, fixing an obvious bug where you're acquiring the same lock twice in four lines of code. So how about this? You wrote a function called foo. Your partner wrote a function called bar. Now foo needs access to resource one and because it's a good function because you wrote it, it grabs a lock for resource one. However, while it's using resource one, it also calls bar. Now bar, which your partner wrote, also knows that it uses resource one. So what does bar do? It tries to lock resource one. Now it's possible that in certain cases, bar might be called when it doesn't hold a lock for resource one, right? In which case it needs to lock the resource. It's also possible that foo in certain cases would be called net way, right? So particularly when you guys start making calls, you know, into nested code, this can happen, right? Is there a solution to this problem? What does it call? People know about this, yeah. What's that? Well, but do I hold this, I know where you're going with this and it doesn't end well, right? Because if I check whether the lock's already held and then don't acquire it, then that doesn't always work in certain cases. But there's something else I can do, yeah. What's that? Yeah, don't worry about argument passing. Just tell me how to do this, right? We can rewrite the lock interfaces needed to fix this problem, right? What do I need to do, yeah? Yeah, so there's something called a recursive lock. It all comes back to recursion, right? A recursive lock is not necessarily written to allow you to recurse on it, but a recursive lock will track the number of times it has been acquired. And then it will also track the number of times it has been released because recursive locks usually require that those two things are paired, right? So that's the problem with checking if you hold the lock because then when you call release, you sometimes end up returning to a function where you've released a lock that the function doesn't know about, right? And if you want to, if you find yourselves, this won't happen for assignment one, but if you find yourself for assignment two or assignment three struggling with the problem where you need this sort of feature, I have a suggestion, implement recursive locks, not that hard and may come in handy in a few places. On the other hand, please don't suddenly make the locks that we gave you recursive because that will make a lot of things unhappy, including the auto grader. So we don't want that, right? Okay, so let's go back to the more interesting case of deadlock where it's multiple threats, all right? And when you guys start dealing with deadlock, there are four conditions that are required for deadlock to happen. The first is that I actually need to have shared resources. If I don't have locks, I don't have deadlock. The second is that I can't preempt resources. So the system doesn't have a mechanism for taking resources away from a thread that already has them in order to allow the threads to make progress. For example, if I had a way of noticing that two threads had deadlocked, I could yank one resource away from one of them and allow the other one to proceed, right? If I don't have that, then I might be in trouble, right? Multiple independent requests means that I can hold one resource while requesting another resource. If I always had to request all the resources I needed at once, then that makes some of this a lot easier, okay? And the final thing is, and this is the easiest way to break out of deadlock normally, I need a circular dependency graph. No matter how many threads are holding a lock, that graph starts and ends at one thread, right? And that's what creates this sort of circular loop that leads to an indefinite sleep condition, all right? So this is, I feel sort of obligated to talk about the dining philosophers problem because how many people have heard about this before? I'll be mostly graduate students, yeah, see, I told you. So the undergraduates, I'll have to hear about it because it's the obligatory problem, deadlock problem. So imagine I have five philosophers that are trying to eat together. This is a historic impossibility, but just bear with me. They have five, how many chopsticks? Five chopsticks. And yeah, so they have five chopsticks and they are trying to have dinner together and here's what happens. So this guy grabs a chopstick, this guy grabs a chopstick. You can see the algorithm they're using to grab chopsticks. This guy grabs a chopstick. Oh, sorry. I forgot to tell you. They need two chopsticks to eat, right? I was about to say, how does this work again, man? It's such a classic problem that I forget about it every year, okay, so anyway. So now they all have one chopstick. Clearly, they need two resources and now they're stuck, right? Because no matter who tries to acquire another chopstick, they're all going to be stuck in this synchronization, in this loop, right? So what do I do now, okay? So let me use this to illustrate the four ways to break out of deadlock, right? And those four ways essentially mean relaxing one of the requirements for deadlock. So for example, if I don't wait for another chopstick, then I can't sleep, right? So the way this would work is I get one chopstick and then I immediately try to get another chopstick and if I can't get that chopstick, I put down the first one. So if you go back to that example, you can see that if anybody at the table did that, the whole system breaks down, right? Somebody gets two chopsticks, you know, and then we can all move forward, right? That person can eat, they can go out there, chopsticks, everybody else can keep going, right? I break cycles. So this is probably the most useful way of breaking out of deadlock. Breaking cycles means establishing an order in which resources must be acquired that prevents this circular resource dependency graph. So if I numbered the chopsticks from zero to four, or one to five, but we're computer scientists, so let's start at zero. And I required that every philosopher grab the highest number of chopstick first. What you would find is four out of the five, so here's my question. Well, anyway, I already gave away the answer. Four out of the five philosophers would grab the chopstick, for example, on their right first. That's what we just saw happen, okay? If the fifth guy grabs the one on his right, we're in trouble, but because we've established this ordering, it turns out the fifth guy will grab the one, try to grab the one on his left, okay? That one will already be held, you know, he'll fail, the guy next to him will be able to get the second chopstick and then we're good, right? Once one guy gets two chopsticks, then we can make forward progress, okay? So if I establish, now go back to the example I used before with thread A and thread B, we're trying to grab resource one and resource two. How do I use this to fix that problem? Thread A and thread B both need resource one and resource two. What do you have to make sure in order to ensure that those two threads cannot deadlock on each other? Yeah, I have to lock resource one before resource two. It's that simple. That's what caused us the problem before, was that one thread locked resource one and the other thread locked resource two. If both threads locked resources in the same order, they cannot deadlock, right? Very simple. And that's the common case. You don't normally have like five threads that are trying to acquire 10 resources or whatever. This is kind of a silly example. That's why I think it's kind of dumb. But normally it's you have two threads, then you need a couple of different resources, acquire them in a certain order and you're fine, right? Even if I don't need all the resources, let's say there are three resources R1, R2 and R3 and different threads need different subsets of them at different times, as long as they locked them in the same order they cannot deadlock. So even if you grab two and three and I grab one and three, we'll be fine. As long as we have an option on the right. The last thing and these last two solutions require new mechanisms that we don't really have right now. So break out, right? I could see that there's a deadlock at the table, walk over the table and slap one of the philosophers and take their chopstick away, give it to another philosopher and then we'd be fine, right? But again, we don't really have that. There's no like lock slap call that you can do to like get someone to drop the lock, right? You can try to implement that. I don't know how it would work but so we really don't have a way to do this and then also don't make multiple independent requests if I could figure out how to force the philosophers to grab both chopsticks at once. Then again, I don't have this problem but we need a mechanism for doing that. So again, you would need like a way to acquire simultaneously multiple locks. That you might be able to do, especially if it was like two or something but it would be probably hard to write that in C so that it's a general. Any questions about dead lock? So starvation and then this is something that's gonna be useful for you guys when you do your reader writer locks in particular. So dead lock is one way we can get into trouble with synchronization primitives. Another one is called starvation. So starvation is a case where not always because of synchronization primitives but sometimes because of the use of synchronization primitives, some threads can be put into a place where they don't make forward progress. They're not blocked, they're not necessarily sleeping, they may be running, but they are not making progress and there is some condition that I can establish in the system where they will never make forward progress. Does anyone wanna identify this condition with your reader writer locks? Is it pretty, yeah. Yeah, so naive invitations of reader writer locks frequently have the problem that readers can very easily starve writers. The reason is that I can have multiple readers using the resource at the same time. So imagine I've got 100 reader threads and they're constantly reading, they read a little bit and then they do some work and then they read again. And so the problem is there's always like a non-zero number of them that are reading and because they're reading it's okay for them to read at the same time and so they're constantly, there's like it's, I don't know, I mean it's like if you had a, that's not a very good analogy, I'll stop, I'll stop before I get myself into trouble. But you know, like there's always some readers in the room and the writer can never get in there, right? And the writer's waiting outside, a reader comes in, a couple more readers go out, 10 more readers go in. The writer's just sitting there constantly like, what is it gonna be my turn? And so that's a case of starvation, right? You've implemented your reader writer locks in a way that the writers will never make forward progress as long as they're readers, right? And when you guys do your reader writer lock implementation you need to figure out how you wanna handle that. I will warn you the solutions are not, will not necessarily be elegant enough to satisfy you, right? They're, some of them are sort of hackier gross but you do need to make sure that you have a mix of readers and writers going, right? And that's a case where it's very easy to have starvation. And again, starvation is different than the deadlock in that there are threads that are making progress. So in a reader writer lock example, the reader threads are making progress. It's just some threads are not, right? The writer threads are being starved. All right, so, so, you know, race conditions came about from not using synchronization primitives correctly. Deadlock occurs frequently sometimes from an overzealous use of synchronization primitives, right? Maybe locking in certain cases where you don't need to. Which one would you prefer? You're the system designer. I'm interviewing you for a job. I say, which do you prefer? A race condition or deadlock? Who votes for race condition? Okay, what are you guys doing? Who votes for deadlock? Right, why? You can detect it, right? A race condition can manifest itself in all sort of weird ways, right? A deadlock always manifests itself in the same way, which is that there's a group of threads or the entire machine that's not making forward progress, right? So a race condition, you might run your system a thousand times and it only fails in that way once. But the deadlock, frequently, that'll deadlock every single time or a large number of times, right? And once it deadlocks, it doesn't just crash, it sits there smiling at you, right? Like, waiting for you to figure out what went wrong. And that can be a pretty useful state for the machine to be in, particularly if it's like a real piece of hardware or something like that, right? All right, so, any questions on deadlock and starvation before we go through an example with some synchronization? All right. Okay, so, let's talk about how to build a synchronized, bounded buffer producer-consumer problem using condition variables. So whenever you sit down with a problem like this, my suggestion is, first, try to identify as much about the problem as you can. Okay, so the problem statement is that we have a fixed size buffer that is shared by two types of threads. There are producers and consumers. Producers put items into the buffer. Consumers remove items from the buffer. And, obviously, the buffer is fixed size. So if a producer tries to add an item to the buffer when it's full, what does the producer have to do? Wait, there's no room in the buffer, okay? Same thing for a consumer. A consumer cannot consume an item that doesn't exist in the buffer. And so if a consumer finds a buffer that is empty, the consumer has to do what? Wait, all right? Okay. So once you sort of outline the problem, the next step is to think, what do I want to happen? What does success look like? What are the things that I want to ensure? In this case, I need to make sure that the producer waits if the buffer is full, okay? I also need to make sure the consumer waits if the buffer is empty. These are just correctness conditions. Finally, and these are the things that are usually hard to get right, right? If there is room to put an item in the buffer, then there should be a producer waiting to put an item in the buffer, right? I shouldn't have a condition where all the producers are sleeping if there's space in the buffer, right? Because I want to make sure that items make it into the buffer as fast as possible so they can be consumed, right? In the worst case, if this went wrong, I could have an empty buffer, a bunch of sleeping producers, and a bunch of sleepy consumers, and the whole thing just shuts down, right? So I don't want this whole thing to melt down and essentially just stop, right? I need to make sure that everyone keeps making forward progress. Same thing with consumers, right? So if there's something to consume in the buffer, there should be a consumer who's waiting to consume it. I don't want all the consumers, if all the consumers somehow get put to sleep despite the fact that there are items in the buffer, what will eventually happen is the buffer will fill, all the producers will go to sleep, and then I have the same problem where I'm not making any progress, okay? So here's my little bit of pseudocode, right? I have two functions I have produced, which takes an item, and I have consumed which returns an item. Don't worry about finding the item in the buffer or whatever, it's uninteresting, right? All I'm worried about is the synchronization required to get the communication between these two threads to work. So I've identified the conditions here for you, right? So what are the conditions that indicate that the consumer or producer need to wait? What tells the consumer that it needs to wait? If the buffer is empty, so how do I represent that in the code? Count is equal to zero, right? Count is a piece of shared state that's going to track the number of items in the buffer, right? How do I know that the buffer is full? If count's full, right? Some magic number I just threw in there, right? So the buffer has some size full is equal to the size of the buffer, okay? So I've already told you we're gonna use condition variables. Hopefully that feels like a natural fit for this problem, right? I can't necessarily solve this problem with just a lock, and semaphores don't feel like a perfect fit. I actually, I think there's a way to solve this using two semaphores, but once you have to use more than two semaphores, you know you're into like weird territory, right? At that point, you're just using semaphores just to say, hey, mom, I used a semaphore, right? There's no real reason to use it at that point, right? Don't, again, especially if you have two semaphores with the same problem, just like really have to question yourself at that point. Okay, so we're gonna use a condition variable. The variable is the count and the two conditions that require waiting are producers, full, consumers, empty, okay? So here's my first cutout. And I've created a couple of global variables that I'll just assume get initialized at some point off screen. I have a count CV and I have a lock, right? Because the CVs that you guys are using always have a lock associated with it, right? The lock is what I use to make sure that the condition doesn't change while I'm checking before I go to sleep, right? Okay, so here's what I'm gonna do. I hit the top of the produce. I'm gonna acquire the count lock. Now I need to acquire the count lock before I modify the count, right? I mean, the count's a shared variable. Even if I didn't care about trying to coordinate the producers and consumers, I would still need to do this to make sure the value of the count remained same, okay? You may think that this looks like a atomic operation, but it's not, right? It's not an atomic operation. You don't know that. Let's put it that way. It could be, but it's probably not. So in the producer, I acquire the count lock. While the counts fall, I wait on the condition variable. Same thing for the, sorry, for the producer, while the counts fall, I wait for there to be space in the buffer, and in the consumer, I wait for the buffer to have items in it. Does this work? No. Why not? What's that? What's missing from this? There's no notifier signal, right? What's gonna wake up these dudes, right? Like CV weight, CV weight, okay? So that's one hit when you approach synchronization problems, right? Pair, waiting, sorry, pair sleeping and waking. If you have an acquire for a lock, you need a release. If you have a CV weight, you need a CV signal somewhere, right? I mean, if you, CV weight without a CV signal, that threat is never waking up, ever, right? Now, I mean, it's gonna be tricky to pair the two properly, but if you don't have one at all, then you're just in trouble, right? Yeah, CV broadcast without a CV weight just sort of pointless, right? Like there's nobody waiting anyway, who cares? Okay, so I need a CV signal or broadcast, okay? Where am I gonna put that? Where does, where's the right place to call signal or broadcast? Let's not worry about which one I'm gonna use right now, but where do I wanna call signal or broadcast? Yeah, okay, so in theory, so remember my condition here is buffer full, buffer empty. Does that condition change every time the count changes? Okay, does that condition change every time the count changes? What's that? Well, okay, so there's, yeah, so there's clearly a pairing between the direction I'm pushing the count and a transition between full and empty, but isn't there another state of the buffer? I mean, the states I care about signaling about are full and empty, but isn't there another state? Like partially full, right? I mean, I could be full, I can be empty, but every other value for the count is neither full nor empty, right? So if I removed, if the buffer had eight items and I started to move the sixth item, the buffer was not full and it's still not full, okay? So again, how do I identify transitions between the states I care about, right? So I really, unless the buffer only has one item in it, I really only ever transition between full and not full, right? And between empty and not empty. I can't transition between full and empty directly. So what's the transition that indicates that I just went from full to not full? How do I identify that? Okay, so what was the count when I started? So first of all, what function causes the buffer to go from full to not full? Produce or consume? Consume, right? So how do I know that the buffer was full if I started to consume? The count was equal to full, right? Same condition I use for the producer in order to put it to sleep, right? So I'll just sprint to the end here. So this is pretty good, right? So, and actually, sorry, I got a little bit ahead of myself, so this will actually work, right? So what I'm doing, as Gila suggested, is when I increment the count, this is when the count changes, right? So I'm gonna broadcast every time, okay? Down here, same thing. Count changes, I'm gonna broadcast every time, right? But I started to hint at the fact that there's a problem with this, right? And the problem is essentially that I'm calling CV broadcast a lot when I don't need to, right? I don't really care if I just took the, you know, enthite, you know, the fourth item out of an eight item buffer, right? There shouldn't be any producers that are sleeping at that point, so I don't need to wake them up, right? So the cleverest way to do this is to do this, right? Down here, if the count is equal to one, it means the producer just put the first item into an empty buffer, right? And it's time to wake up the consumers, right? Same thing down here. If the count is now full minus one, it means I consumed the full item, it's not a word, the fullest item of a full buffer. And there is now space in the buffer, so it's time to wake up the producers, okay? As a last note, I'll leave you guys to think about why not, why I wouldn't necessarily want to use CV signal in this particular case. I'll just leave that as an exercise for the view, okay? I think we're almost out of time. Put me a look, such an awkward walk all the way over here. Oh, I have three minutes, awesome, okay. So again, my tip with using semaphores is usually don't, right? Because semaphores, looser semantics, semaphores not having an owner means that they're typically more difficult to get right than a lock, right? Definitely don't use a binary semaphore as a lock, right, it's just dumb. You want ownership. You want the ownership because it helps you out, it helps you correct programming mistakes that semaphores are not going to, okay? However, they can be more natural in cases where you have enumerated resources, not necessarily super common. But in general, if you guys are reading for a semaphore particularly in this class, think twice, right? Ask somebody about it and we'll talk. However, there are times when semaphores are useful, right? So one place to look for a useful way to use a semaphore is in your driver code. Every one of the tests that we've given you that forks off multiple threads uses a semaphore to wait until all of the threads are finished. And it's pretty clever, right? So what it does is when it's forking off threads, it starts, it initializes a semaphore that has a value equal to the number of, sorry, it initializes a semaphore to zero as it's forking off threads, it ups the semaphore a bunch of times. No, false. Yeah, there we go. Okay, here we go. Phew, okay. So initialize the semaphore to zero. It passes the semaphore to each thread. When the thread, after it creates all the threads, it wants to wait until they've all exited. So what does it do? It sits in a loop and it tries to pee the semaphore the same number of times it's the threads that it created. Okay? The first time it pees the semaphore, let's imagine none of the threads have exited, it's gonna block, right? And then what's gonna happen is, as the threads exit, they're gonna be the semaphore. So every time a thread exits that this driver created, it wakes up and pees the semaphore again. And it keeps doing that until it's peed the semaphore the same number of times of the number of threads it created and that means that all the threads it created have exited and it can continue. Okay? I probably didn't explain that as well as I could have, but go look at the code it's in. Okay? Let's not go through barriers. Okay, so last, the barrier slide you guys can look at. If you want a tip, I can put that on Piazza, but that's just a little bit of cleverness. So final notes, right? So a lot of problems can be solved with a variety of different synchronization primitives, but there's an art to choosing the right one, right? And usually there's one primitive that's gonna feel more appropriate than others. If you start doing a synchronization problem with the wrong primitive, it's gonna feel kind of disgusting, right? You're gonna feel like you're having to do a bunch of things like use multiple of the same primitive, like five, I solved the problem with five semaphores, right? There's probably a better way to do it, right? And so you guys will have a chance to practice this this week. All right, this we already talked about, so I'm done for today. Hope you guys enjoy the rest of the week. Good luck on assignment one, and I will see you next Monday.