 Hello everybody Welcome to a quick little supplemental for a lecture from last night I didn't quite get through all of the topics I wanted to and so I thought I would record this just to help out a little bit maybe for for your laboratory number one and putting do their project one design docs so what we were talking about yesterday was Concurrency and mutual exclusion and we're sort of starting that whole process of how to get synchronization to work properly And I just wanted to finish up that discussion If you remember we sort of introduced this issue that well threads Can give us the ability to have Overlapped from IO and computation and therefore seem like a really nice Efficient way to have multiple things happening at once We run into an interesting issue. So I had introduced this Banking Example where for instance what we did on a deposit was we would get the account information We'd increment it because maybe we were doing a deposit and then we'd store it back and the reason threads were really going to help us out here is because if one user was You know their account was stuck waiting for disk IO another user could be adding to the balance or whatever And so this was our motivation for going to threads except as you see here the threads Encounter the fact that the accounts are shared state and so in this particular instance We show an example here where perhaps thread one gets the balance and then thread two gets to run and it grabs the balance increments and Stores it back and then thread one gets to run again and as a result The whole operation that was thread two is erased because thread ones Overwrites it and if you remember I talked about the malicious scheduler viewpoint, which is you need to view The fact that there there's a malicious Murphy's law scheduler running anytime you have multiple threads working on the shared data that will find a way to Find that sequence that corrupts your data and it will do so at the worst possible time. So in this instance our Malicious scheduler found a way to split up thread one and thread two Okay, and exactly the wrong times now What we clearly need to do is we need to put an atomic section in here Which may basically says that three instructions load add in store all become atomically bound together and can't be interleaved Okay, and so to do that we talked about locks and locks have come up earlier in the in the term as well And a lock is in general prevents somebody from doing something Excuse me and in the context here you imagine locking before entering a critical section Unlocking when you leave and waiting if something's locked and so the important idea that You ought to get from these lectures on synchronization is that pretty much all synchronization problems are solved by waiting So if you look back to our bad example here The fact that thread one started doing something and then thread two popped in and screwed everything up And then thread one got to go again if thread two were to just wait Until thread one was done with this atomic section. We would have resolved this particular bad behavior. Okay, and so all Synchronization problems can be solved by waiting and the trick is to wait only as much as you need to not too much And we'll talk a lot about Examples where you wait too long and of course when we talked about locks yesterday. I mentioned the fact that you need to Allocate and initialize a lock so you could for instance on the left here You might declare a my lock structure and run a lock in it on a pointer to it or on the on the right side You might declare P thread mutex T and then initialize it in one way or another that depends what on Which type of locks you're using but then once you've done that the locks provide a couple of atomic operations One is a choir which you know For instance in C syntax would take a pointer to the lock you're acquiring and when you do that You're wait until the lock is free and then you Grab it and if you try to acquire the lock When somebody else has marked it as busy then you wait and wait in this context is going to mean your thread Is put to sleep so you're not wasting cycles and we'll talk about that in the next lecture and Then when you're done you release the lock which will then free up potentially somebody who might be waiting and so Looking at our banking problem again. What you see here is we identify the critical section So a good critical section here is the fact that we want to have an atomic sequence of this getting of an account incrementing and storing back and we Decorate around it the acquisition and release of the lock And so we acquire the lock at the top and we release it at the bottom and by acquiring and releasing a lock What we've done is we've ensured that only one thread gets to run at any given time in the critical section And that's what we call Asserting mutual exclusion and so just to show you that a little graphically here. We've got an animation Here's the critical section with an acquire and a release and if you have multiple threads that are all trying to get into that critical section So say here thread a b and c what happens is only one of them gets the lock and the other ones Are forced to wait so for instance if thread a is the one that gets the lock What that really means is not only do they mark the lock is busy, but they're allowed through the acquire operation thread b and c are Waiting in a choir. So they their threads the acquire System call or whatever it happens to be we'll talk about many options starting next time They will be waiting there. So they won't emerge from the acquire yet Okay, so that again looking up top here if multiple threads all call deposit at once Only one of them will actually get through the acquire and into the critical section The rest of them will be waiting in the acquire Function call or system call and so what I show you here is that as soon as a exits Then b is allowed to go through and then as soon as b exits then c is allowed to go through And what ordering do things come through the acquire operation? Well, unless it's a special type of block where the semantics are explicitly specified You have to learn Assume that there's a non deterministic choice as to which of the threads that are all waiting on the lock are allowed through it Any given time the important part however being that only one of them is allowed through Okay, and to Circle back and finish up this example in order to make this really work The it's the account that is the shared data here. And so we need to make sure that all All code that accesses the account is protected by the same Lock, okay, so for instance, there might be a Withdraw here or there might be a An initialize Or some other account operations We need to make sure that they all use a lock From an acquire release around a critical section and in particular they need to use the same lock Now some definitions that we had last time last night as well So synchronization is basically using atomic operations where an atomic operation is a sequence of non interruptible Uh, non interruptible instructions to ensure cooperation between threads to make sure that we don't get undefined behavior Okay, and mutual exclusion is the technique that we talked about here where by putting locks around a critical section And making sure that we exclude all but one thread at a time through that critical section Then we can make sure that we have an atomic operation there and that our synchronization works Okay, and so that critical section is typically the piece of code That's being protected by an acquire and release of a lock And uh, we put locks around it to get mutual exclusion to give us our synchronization Okay Now here's another concurrent program example You got two threads a and b they compete with each other one tries to increment the shared counter the other tries to Decrement it And then we've got this kind of a free for all the thread a and thread b Show here. Uh, we have uh, basically they're sharing the same variable i so in this instance It's a global variable, but thread a sets it to zero and thread b sets it to zero So they're both setting the same shared variable to zero then We're in a while loop and sort of while i is less than 10 for a it sort of increments i and b says well while i is greater than minus 10 it decrements And uh, whoever wins gets to say a wins or b wins, okay And we're going to assume that memory loads and stores are atomic But uh incrementing decrementing are not atomic and so from that standpoint There's no difference between i equal i plus one and i plus plus those compiled of the same underlying instructions Which is a multi instruction sequence. Okay in most cases Um and so what happens here is uh, well either of them could win and in fact, we've got kind of A funny scenario where it's not even guaranteed that anyone can win. Okay, because um, if you look at Uh a hand simulation of this example We could look at the inner loop and here we have the example thread a and b thread a Might load from wherever i is and into register One which maybe it's got a zero there thread b does the same thing. We got a zero remember we initialized i to zero and then thread a basically adds one to it and meanwhile thread b Subtracts one And how could we get this perfect interleaving? Well In this perfect interleaving could happen if we have two Two cores that are running or maybe we have hyper threading which we talked about last night as well And then of course thread a goes ahead and stores a one Because it added one to zero and got one but thread b now stores a minus one and notice that because of this interleaving thread b Ended up completely overriding the result of thread a so thread a went to all this trouble and then nothing happened Okay, because thread b overwrote it. So this is clearly a failure of atomic sections You know and you can imagine this race and where off a gets off to an early start b says Better go fast and tries really hard a goes ahead and writes one then b goes and writes minus one and a says I could have sworn I put a one there Okay, this is uh indicative of the types of problems that happen when you've got data races going on Data races. I'll show you in a second here is basically two threads attempting to access the same data So that's basically this memory location m of i Where one of them's a right and here we have a situation where Two of them are rights. Okay, so that's a data race and the notion of simultaneous Is really defined even when you only have a single cpu and you can't have simultaneous execution Like this shows here in this example above but the scheduler could switch out at any time so you have effectively have all of the Liabilities as if you had simultaneous execution So this may be concurrent but not parallel But it still behaves badly even in those instances. Those are race conditions So we could pull out our locks now and we could say well here. I'm going to put a choir and release around the Around the increment or decrement and now did we do better? Okay Well here now we no longer have an example where a thought it was incrementing but ended up Doing nothing because b overrided overwrote it because we've got locks So thread a gets to the acquire first and it's busy incrementing then thread b gets to acquire It's going to have to wait until a is done Then it'll release the lock and then b will get to go through the acquire and do its decrement So each increment and decrement operations now atomic That's good. Okay And in many cases This might be what you want Technically, there's no longer any race condition here because it's never possible for thread a and thread b to be simultaneously accessing I When one of them's a right and why is that? Well, because the simultaneous access Can't happen because the the locking is going on here. All right, but Um, the program is still broken potentially Um, because this is uncontrolled. Okay, uh a and b are just incrementing decrementing incrementing decrementing There's really no Control as to how many loops there are or who wins and so Maybe technically you've gotten rid of the race condition in the middle. Although there is this Looking at it in the wild loop. So I suppose maybe you could still call it a race condition But it's probably not really what you wanted This is really still a bad program The one instance where you might want something like this not with this loop But maybe the i equal i plus one with a lock around it Is for instance when you might have a hundred threads that are all working on some part of a problem And each one of them wants to get a unique number once it starts then they could call An atomic section like this which Does an i equal i plus one and returns the result back to the caller and now each of the threads Some thread will get one from some thread will get two some thread will get three some thread will get a hundred If you do that then this could be an okay use Of something like this And it turns out actually there are atomic instructions that don't even require you to do the lock and unlock in those instances So one more locking example Here is this red black tree that we talked about in one of our early lectures And i also mentioned this last night and in this instance This tree is balanced in a very special way that the red black Algorithm maintains Okay, as you're inserting and deleting elements and if we allow uncontrolled Access the molten 80 or race conditions to to screw up the structure of the tree Then it's not going to work properly anymore. It's not going to have the level of balance It's supposed to have and so what we can do is we can put a single lock at the root and Just make sure that before we touch the tree at all we acquire the lock Here for instance insert the number three and then release it Maybe over on thread b we could we want to insert four we could acquire the lock Insert release maybe want to get the number six we could acquire the lock search for six and release and what we've done is by putting Acquire and release of the same lock around all operations. We make sure that at most Only one thread is ever manipulating the tree Okay, and so our critical sections are Anytime the tree is accessed Either read or written we put locking around it and therefore we make sure That the correctness of the tree algorithm is as good as a uniprocessor non multi-threaded version okay, and so this is A good use of locking even though the threads are busy adding and removing things In this instance it makes sense because the different threads might be grabbing data from the network somewhere adding it to the tree Searching because of some network query looking in the tree, whatever Deleting from the tree that makes some sense if we have all of these threads doing parallel operations And we make sure that the tree that's at the core of that algorithm is stable Okay, and So this makes sense You might say this is a little slow because if you have a lot of threads Maybe most of them are waiting in the acquire of their you know of their operations and so Maybe most of the threads are waiting and so then you can start to ask a question Is there a way to make this faster? While the answer might be yes That answer might be while you lock a certain path in the tree So you put a lock on every node And when you're searching or you're modifying then as you go down the tree You lock the nodes so that anybody else who tries to go there Doesn't encounter the same locks that you do they might be able to work in parallel You got to do that very carefully. Okay Um, for instance, if you always start by locking the top lock and then work your way down Then of course you haven't gained anything so that while there are ways to do locks on Lots of nodes in a tree like structure in a way that keeps the Keeps the data structure consistent under a variety of different simultaneous operations You have to be very careful to do it So this idea of putting a single lock at the root Hopefully makes perfect sense to everybody. You know for a fact that that'll always be consistent the moment you start trying to Parallelize this and allow more than one thread Modifying and into the tree at a time then you got to be really careful And you can start talking about maybe somebody who's reading Does some advisory? Um It does some advisory locking that's all about reads just so that if a writer were to come along They are not allowed to touch the part of the tree that the writer Um that the reader is in and maybe that's an okay way to get some parallelism But I just wanted to warn you that if you go down this path, you got to make sure you're careful what you're doing okay enough on that Let's ask ourselves if locking is going to be the general answer. Okay, and I'll tell you right now. It's not Locking is a way to do synchronization and it's a way to ensure critical sections It's not always the easiest thing to do. So let's look at this producer consumer idea where we have a buffer that's finite size and we may have many Producers of data that want to put the data on the buffer and many consumers And the producers Can produce things and the consumers can consume things running perfectly in parallel And all that we really want to do is we want to make sure that if the buffer is entirely full then producers are put to sleep uh, because they can't put anything in a full buffer and Similarly if the buffer is completely empty then a consumer gets put to sleep because it can't take anything off of an empty buffer So we want to make sure that there's still correctness here Okay, and we certainly don't want the producer and consumer to have to work in lockstep So, uh, we want to do something That's a little more sophisticated than every Uh producer or consumer first grabs a lock That's associated with the whole buffer and then releases the lock. Okay, that's going to put us in that same problem That we kind of saw with the tree data structure. So what are we going to do? Okay, and there's many examples of producer consumer. We talked about pipes Which i'm loosely showing you here with my gcc compiler example Where the c preprocessor and and the first and second phases of the compiler than the assembler and then the loader All feed into each other and one produces results that are forwarded through a Buffer to the next to the next to the next that's a great example of this bounded buffer The example i'm going to do here just because it's fun is a coke machine The producer can put in only a limited number of coke bottles because the machine only holds so many the consumers can't take Coke bottles out of an empty machine. And so, um, what do we do? Okay, and examples other examples are web servers and routers and you name it this bounded buffer is a good example Okay, so here's an example of a circular buffer data structure where we have a right pointer and a read pointer And we set this up so that um the read pointer kind of points to The next thing to be read off the queue And if you keep reading you'll circularly wrap around And if the if the read pointer ever runs into the right pointer, then it knows that there's no data there And similarly if the right pointer ever runs into the read pointer, it knows that things are full Okay And so, uh, the you know the start on this is there's a buffer structure There's two integers a right index and a read index and then there's an array I'm roughly saying, you know of some type star entries that's a buffer size And notice that this is not a valid c code. Obviously, you can't say Arrow type arrow, although you might in some other language And so we might ask some questions. How do we know if it's full on insert or empty on remove? And what do you do if it is? You need to put threads to sleep Put the producer to sleep or the consumer to sleep And what do we actually need for our atomic operations? Okay, so this is a clear question That comes up based on what I said there earlier. And so, uh, here's our first cut Okay, we'll have a mutex which is a lock on the buffer. It's initially unlocked And the producer might do something like this. They grab the lock They sort of spin in a loop saying well, well, the buffer is full Don't do anything. Okay, because remember a producer can't put data into a full buffer And then once it's no longer full we in queue An item on the queue and then we release the buffer lock And then a consumer looks similarly where we acquire the lock on the buffer We wait and as long as it's empty, nothing happens. Otherwise, we know that it's not empty. That means we can dequeue And then we're going to release the buffer lock When we're done and return the item Okay, and so notice that what we've got here is When the producer can't put anything because things are full, we're going to spin And when the consumer can't get anything because it's empty, we'll spin And so that's the weight, right? So remember I said all Synchronization problems The solution has some form of waiting This looks like this helps us. Okay But Not so well if you think about it because look at this the producer Acquires the lock and then goes into a spinning weight loop Then they have the lock acquired and they're in an infinite loop, which means they're waiting for the buffer to Get emptied a little bit except that if a consumer comes along It's not going to be able to acquire the lock because the producer's got the lock So this consumer is going to go to sleep waiting to acquire the lock forever And this producer will be spinning forever and we've effectively got a deadlock here Okay, so or it's really technically a live lock um, but it's a live lock that can't resolve and so This is uh, not a good solution So we got to do something else. Okay, so here might be a solution Uh, and if you notice what's different here is the producer acquires the lock And then says well if the buffer is full i'm going to quickly release and then reacquire the lock And then check again and release and acquire And so notice and the consumer's got a similar idea here And if you notice why is this better? Well, this is better because Let's suppose that the producer is trying to put something on a full queue They first acquire the lock. They notice the queue is full And at that point they release the lock Okay, they reacquire it and then they check again and they keep doing that over and over again until the buffer is not full And then they continue and the reason this works Not very gracefully and not very well is that the consumer Let's suppose that things are full. So the producer acquires the lock notices. They are full The consumer comes along and yes, it could acquire the lock or try remember in our last example They couldn't because the producer was holding the lock but the if the consumer comes along and goes to sleep sleep in the acquire then um The moment that the producer says oh buffer full release It releases the lock at which point the consumer comes out of the acquire and now it has the lock Okay, and it's going to notice probably that the buffer is not empty because we know it was full And then it can dequeue and go on and so that release Is actually going to release and let the consumer go and this reacquire will potentially temporarily go to sleep Until the consumer here Finishes dequeuing and then releasing at which point will come out of the acquire We'll notice the buffers no longer full will enqueue and go on so surprisingly This works okay, and this actually Works in a variety of circumstances, but it's not great because notice that we're we're burning a whole lot of cycles So if there are no consumers what happens with the producer that's Encountering a full buffer is it's busy running release acquire release acquire as fast as it can And it's wasting cpu cycles to do nothing. So this is a form of busy waiting Okay And so this isn't really going to help us much now you almost you you might also ask well, will this work on a single core? uh, and the answer is well if you think of the idea of trying to acquire a lock when You know when somebody else has it as you got to go to sleep Then what happens there as we go into the scheduler we talked about last night And in that scheduler at that point we effectively relinquish The lock excuse me effectively relinquish the cpu and at that point we Somebody else gets to run which could potentially be the consumer in which case if they're ready to run They'll dequeue and then when we get to run again, we'll acquire the lock and enqueue So this is actually going to work on a single core and it's also going to work on a multiple core But this really is wasting a whole bunch of cpu time. So this isn't great either And uh, so what else are we going to do so notice that If we actually go to sleep in an acquire, we're not wasting cpu The problem with this solution is we're spinning Where we're if there's only the producer a single producer and the buffer is full Then we release and acquire and release and acquire and we just keep going wasting cycles forever And uh, those cycles potentially could be used by some other Code that ultimately becomes a consumer which will resolve the producer and we call that a busy wait Talk about that uh next time on monday so That's this little busy wait symbol Okay, we're waiting. We're waiting. We're waiting. We're spinning. We're spinning. We're wasting cycles Okay, so we need something else and this is really just indicative of the general problem that locks while they're generally powerful enough to do pretty much anything aren't quite the right High level api to do what we want. So we would like a way to do something like this That lets us do a better job of managing resources Um Than a lock. Okay And so higher level primitives and locks we're going to talk about a couple of them Uh as we move forward in the next couple lectures But we can ask ourselves what the right abstraction is for synchronizing threads that share memory now clearly we said that a lock could be Used in a way that allows us to share memory under a wide variety of circumstances But uh, you have to admit that this particular Spinning code here is not all that intuitive and certainly isn't all that Good use of resources So maybe we want something else and we want something as high level as possible where I think of locks as lower level Okay, and so good Primitives and practices are going to be very important because the easier the code is to read and understand The more likely you are to have it correct by design um And so this is important Okay, and it's really hard to find bugs in Multithreaded code that shares data and uh, unix, you know different variants of unix are pretty stable now But it was very common That um unix systems would just crash every week or so because of concurrency bugs and that was just what people accepted okay So synchronization is a way of coordinating multiple concurrent activities and um, we're going to talk about In the next several lectures different ways of synchronizing that are a little bit more intuitive and more likely to be correct Okay, so that leads us to semaphores, which is the topic I wanted to get to today in this special segment And if you remember I met I introduced semaphores a bit, uh A couple of lectures ago, but a semaphore is a kind of generalized lock The term comes from these, uh traffic symbols that you see on railways Okay, and it's the main primitive used in the original unix. It's also used in In pittos and several other operating systems as well And a definition here is that a semaphore Has a non negative integer value and supports two operations one which is uh down or p Is the is the uh standard thing to think about which is an atomic operation that waits for the semaphore to become positive and then decrements it by one And notice for instance, I said here that it has a non negative integer value So that could be zero or or higher And so what down or p does is it waits for the semaphore to become positive So if the semaphore is zero and I execute down I wait And that waiting is one where I go to sleep. It's not a spin wait or a busy wait Okay, and then the moment that it becomes non zero Okay, or positive it then decrements by one and and exits the down or p operation Okay, and then up is sort of the opposite of that which is an atomic operation that increments the semaphore by one And if somebody's sleeping on p, uh, it'll wake them up Okay, and that wake up then will try to decrement by one and if they succeed then one thread will get out Okay, and think of this as a signal operation think of p as a weight operation and p Stands for proberan and v for ferrogan, which is proberan is to test and Ferrogan is to increment in dutch, which is where dykstra named these from Okay, so semaphores are just like integers except While one there's no negative values. So there are whole numbers Two only operations allowed are p and v. So you can't actually read or write the values except initially Okay, so you set it to an initial value and then your only interface is p and v And the operations are atomic. So if you have two p operations on two different threads There's no way for them to decrement below zero So those whatever the implementation is and we haven't gotten the implementation yet It will ensure that there's no way for the semaphore to ever get below zero And for instance a thread going to sleep on a p won't miss a wake up from a v So it won't be the case that there'll be a thread sleeping with a p operation But the semaphore itself is one or more. Okay those that uh interface is ensured because p and v are atomic Now posix actually has a semaphore that gives you the ability to read the value After initialization, but technically this is not part of the proper interface Okay, so the proper interface of semaphores have Only p and v after you've initialized, but if you use the posix versions you can read the value as well So the semaphore as I mentioned is from the railway analogy Here is here's an example of a semaphore initialized the two for resource control So this is going to start looking a little different than just locking. So here's a semaphore Here's two tracks and a value of two basically says that we're going to only allow two Trains into the train yard switching yard at once. So when the first one comes along It's going to come along the track and execute a p operation on this semaphore taking its initialized value of two down to one And uh, we'll go from there. So if you notice that first train came along it executed p That succeeded so it got to keep going the next train that comes along will execute p and now the semaphore is equal to zero But that second one succeeded It's only when the third one comes along and tries to execute the p operation that it gets stopped on p So this train here basically executes p and the p hasn't returned yet okay Or the down operation as it's said in some some interfaces hasn't happened yet So what would make it happen? Well when the train exits and executes v Then it's going to increment The uh, so the v operation is going to increment the semaphore and then that incrementing will wake up Uh, somebody's sleeping on the p operation at which point they will decrement back to zero and get to go So if we let the train go this guy increments quickly to one then decrements and now we're back to where we were So what's different here is that we have this idea of more Uh resources like two here. This is basically giving us a way of enforcing the fact that there's only two things that are in this Rail yard. Whereas if you think about what? A lock is about mutual exclusion that allows only one thing into a critical section. Okay, so this is allowing two or more Okay, so there's at least two uses of semaphores. One is mutual exclusion Which is also sometimes called a binary semaphore or a mutex Which is really used, uh, like a lock Okay, and that's why if you look at how do you make a lock in? POSIX they actually call it a mutex. So a mutex in a lock or a mutual exclusion Uh device is essentially a lock Okay, if I set the initial value to one And then I say, uh, I try to do a semaphore p on that semaphore The first one that comes through will decrement it to zero and be busy doing the critical section Any others that come through will now in a counter the fact that the semaphore is equal to zero and won't be able to get through And uh, and therefore they will not be able to go forward Okay Now another use of semaphores is a scheduling constraint So for instance, we saw earlier with the train the idea that we had a scheduling constraint of two Uh items that could be in the rail yard maximum Here for instance, if we set the value to zero of the semaphore Then we get this idea that we can allow a thread to wait for a signal So thread one waits for the signal from thread two What happens is thread two will schedule thread one when the event occurs So here we go. This is kind of like we set the semaphore to zero And then join Basically says, well, I'm going to try to do a semaphore p on the semaphore Assuming that this starts out at zero. That's my initialization Then the thread join operation is going to sleep because it's waiting for that semaphore to become non-zero And then as soon as another thread finishes That will increment the semaphore which will take it above zero Which will wake up the thread join and will get exactly the same behavior as this thread join Okay So revisiting the bounded buffer here for a moment What we see is that we have correctness constraints So the consumer has to wait for the producer to fill buffers Okay, or in the case of thinking about this as a coke machine the You know, you're a student you go to the coke machine. There are no Coke bottles in there. You got to wait. Okay. I don't know. Maybe it's really late So you take a nap in front of the machine until there's somebody to fill the coke machine The producer or the guy bringing the coke bottles has to wait for the consumer to empty the buffers So if they the delivery guy shows up and the machine's full In the in the instance of what we're talking about here for a bounded buffer They're forced to wait until somebody buys a bottle of coke and then they can put another another coke in so we have Two correctness constraints, which are About resources the consumer waits for the producer to fill buffers The producer waits for the consumer to empty buffers and then one more constraint Which is a mutex constraint to make sure that we have correctness on our queue itself And don't have bad behavior And this is going to be just like a lock and it's going to be needed for the same reason We needed a lock at the root of the red black tree in that earlier example Which is for correctness. We want to make sure that the queue doesn't get screwed up Okay, and the reason again, I just said this but we need that mutual exclusion Is because you know computers are stupid And if you have multiple threads both trying to manipulate the the reader and the writer Part of the interface then you're going to get You're going to get bad inconsistent behavior and there might be other more complicated things in this instance Maybe the input puts things into a heap and the output takes the One with the smallest value out of the heap So there's many instances of this bounded buffer That you could think of that are more sophisticated than just FIFO All right, so general rule of thumb you got to use a separate semaphore for each constraint So we have a semaphore for the full buffer constraint a semaphore for the empty buffer constraint and one for the mutex So that's three semaphores And we're going to start out with no full slots because the machine is empty We're going to start out with uh 100 empty slots because the machine is empty And the mutex we're going to start out with it set to one Because uh we're interested in uh using this as a lock or a mutual exclusion And so then our code is pretty simplistic and set straightforward. So the producer Comes along and says oh, let's first execute a semaphore p on empty slots So what this says is if the number of empty slots is Uh zero because they're the machine is full. We're going to sleep here At that semaphore p Okay, so the producer can't actually add any coke machine bottles to the coke machine If there are no empty slots assuming there were empty slots Then what the semaphore p does is it decrements the number of empty slots? Why because we're about to add another uh We're about to add another coke bottle. So there's one less empty slots And then notice that we grab the mutex with a semaphore p and we release it after we're done And that's all to protect this Queuing operation we're going to enqueue a coke into the machine Or enqueue an item on the buffer Okay, and why do we have a semaphore p follow and have semaphore v because the operation on enqueuing We don't we can't afford to have multiple threads screwing it up Okay, so this is think of a mutex as a lock And the consumer is kind of the mirror image of this right so the consumer say you're a student grabbing a bottle of coke Says that if there are no full slots because the number of full slots is zero This semaphore p is going to go to sleep Otherwise if there's more than zero full slots That means there's more than one and more than zero bottles of coke Then the semaphore p operation will decrement the number of full slots exit We have our mutex around the dq operation. So we grab Um the lock by doing a semaphore p and then we release the lock by doing a semaphore v And we correctly do a dq Okay, and then um finally We when we're done we increment the number of empty slots to tell the producer we need more Okay, and I forgot saying we increment the number of full slots down here in the producer case Okay, so think of These uh as critical sections Okay, or maybe just the enqueue and dq that are being protected by mutexes Okay, so that's one use of semaphores Then this producer when it puts a bottle of coke in Not only does it increment the number of bottles of coke by incrementing full slots But if it turned out that there was a consumer waiting for a coke bottle Then this semaphore v on full slots will wake up Uh an item that was sleeping on a semaphore p Okay, and it could be by the way, I'm sure some of you are thinking well, what if there are Five students sleeping on semaphore p What happens is it might be the case that the semaphore v going from zero to one wakes them all up But then the first thing they're going to try to do when they're awake is decrement the semaphore One of them will get a chance just because of the scheduler to decrement it Uh from one to zero and they'll exit semaphore p and get to go on The rest of them will encounter that the semaphore p is back to zero already and they'll have to go immediately back to sleep so this Full slot increment will only wake up one of the sleeping guys if in fact Full slots went from zero to one when we did semaphore v And then the flip side of this is this semaphore v on empty slots will wake up the producer If it turns out that um, there is a producer sleeping on Um the fact that there aren't any empty slots for the bottles of coke Okay So this is there to give you an idea that semaphores are a lot more Sophisticated in what they can do and they do they do both mutex operations Like locking and they all and they do resource operations where you get to track the number of resources And make an action based on that So a little discussion about the symmetry Of this solution. So why Do we do semaphore p on empty buffer and semaphore v on full buffer for the producer? But the consumer does the opposite. Well, that's because the producer is Waiting when there's an empty buffer and signaling that they've filled a buffer Whereas the consumer is waiting when there are no full buffers, but signaling when there's a new empty buffer Okay, so we decrease the number of empty slots. We increase the number of occupied slots Here we decrease the number of occupied slots and increase the number of empty slots Notice by the way, I just want to say this That it's not we have two semaphores for either end of the spectrum Okay for whether we can add items To uh to the front or not and whether we can remove them from the back or not those two semaphores Uh are on opposite ends of the buffer. So we need two of them. We can't just get by with a single Uh semaphore that sells us how many items are in there because then we wouldn't be able to sleep On one or the other side. Okay So we need two one for each side of the buffer The other thing to to notice is is the order of these p's important. So the producer did do semaphore p on empty slots and then And then semaphore p on the mutex and then in q and so on Will this matter if I swap these and the answer is yes, this can actually cause deadlock Okay, why is that? Well, if you look the producer comes in Execute semaphore p on the mutex Okay, so it grabs the lock and then it calls and says, oh, there are no empty slots And so it goes to sleep We've now got a situation where the producer is sleeping While holding the lock Which means that if the consumer comes along And tries to add a bottle of coke or takes to take away a bottle of coke. Excuse me What'll happen is it'll execute semaphore p on full slots It'll try to grab the mutex But it can't because the mutex has been grabbed by the producer after Just before it went to sleep. And so the consumer will be permanently stuck All right, and this is a bad deadlock scenario Okay And then you could come up with a cycle We'll talk more about deadlock later in the term is the order of the v's important That's no and the reason is that Neither of these Block in any way what they do is they increment a value and possibly wake somebody up So you can do those in any order Okay, what if we have two producers and two consumers? Well, if you look back at our solution back here What you'll find is this works for any number of simultaneous producers and consumers and The threads will just go to sleep if there's no space And so this is this particular solution works perfectly well for many producers and many consumers Okay Especially the one producer one consumer case, which we might have started with Okay Don't need to change anything. So where are we going with synchronization? So in the next monday and the the rest of this particular term, we're going to be going To various high-level synchronization primitives using atomic operations You're going to see a bunch of hardware to help us. We're going to start with load and store being Atomic and then we're going to disable interrupts As a way of getting locking and then we'll talk about using test and set and compare and swap And then we're going to start putting in some higher level primitives And what I mean by that is we already know what locks and semaphores are but we're going to start talking about How do you build them? Okay, and we'll talk also then about monitors and send and receive and so on more sophistication And then we'll talk about shared programs All right, so that's all I wanted in this supplement Just wanted to talk to you a little bit more about locking and semaphores We'll repeat some of this material on monday, but I just wanted to give you a little bit of an extra Heads up here in case you were interested in learning something more about semaphores before your design doc was due All right. Have a great Rest of your day and we'll see you on monday. Thank you