 All right, you welcome back to operating systems. So today we're talking about centiphores which Also mean fun flags to signal boats to do stuff, but I don't know anything about that, but hey It's kind of interesting So Using that well, that's how they got the name because we're going to use these to do ordering between things so Remember from before that locks they ensure mutual exclusion, which makes sure only one thread executes at a time So between the lock and the unlock that code is called a critical section and only one thread may execute that at a time So if we have that that does not help us at all if we want to ensure ordering between threads, which may be useful So how could we ensure some ordering between threads? So let's take a look at this problem and think what would happen. So we have two threads One is calling print. This is first. The other threads just calling print I'm going second and we want to ensure that both of these threads like we always see this is first And then we always see this is second and that's the only possibility we have so Could we do something about this? So let's look at the code. Let's make sure get restore So here is our ordered print code. So We have We create two threads Thread zero that we want to have start executing print first Thread one we want to have executing print second and then we create them We let them just run as much as they want and then we join them after they have finished and terminated so If I run this I should see that after I recompile I Should see that order so I can see I'm going second. That's probably not good And if I execute it again, well, you know, hey now it works. I see this is first. This is second Well worked again didn't work. I'm like 50 50 right now. So that's not good So knowing what we know now. Is there any way we can guarantee that we first see this is first And then we always see I'm going second after that Hmm Yeah Yeah, so we could make a flag that one thread checks the other one That seems kind of complicated because then we have to make sure it doesn't have any data races or anything in it So I want just quick and dirty based off what we already know Yeah Just what? Just sleep. Okay, not that dirty Yeah, so we're using p threads here. So p threads are kernel threads So we have no choice over their scheduling So if this was your lab for they would run an order because they're all cooperative and there's a set order to things. Yeah Yeah, so I could do something lame like this Right that makes sure we always see this is first and this is second But that'll also only let's one thread execute at a time which is kind of lame because let's assume that oh This takes and then after this takes a thousand seconds So if we did that well, this whole thing is going to take like two thousand seconds as assume prints are it instantaneous Ideally, we want the case where we always just print this print print this and then they can go in parallel So they can each do the thousand second thing at the same time So let's go back. Let's change this back So is there anything I can do for this? so I Will do something slightly crazy and you tell me if I'm right So we know about I don't know we know what a mutex is So I'll create a mutex Let me make sure I initialize or wait Let's just use the static initializer So I have my mutex if I want to Make sure that this Doesn't run well Let me just lock it. So I'll lock the mutex So I essentially grab the key and then I'll do something silly. I'll grab it again So if I grab the key and I get it, I'm going to wait until I can grab it again Hmm seems weird. So I can't make any progress. But what I'll do is In this thread after it prints. This is first What about if it unlocks it would that work? Don't see why not? So Everyone agree this kind of works ish. Maybe Well, yeah, so this is like. Well, let's just run it Okay, so The something there's a bug in my program or something. Oh, yes. He worked No problem. So it worked it worked malicious compliance that worked this is first happened the other one never happened but I Guess you only said that Yeah, you only said that this is first has to print before this is second. That's still true. It just doesn't print So what happened there? So it kind of worked So in the case that thread to executed first well, it would lock Lock that mutex. So it has the key and then wait for it to be able to unlock it again So it can't print this print f-line until this first thread prints this is first and then unlocks it in which case this can Successfully make it pass lock and we get the second line. So that's when it worked So the case where it didn't work is well if thread one executes first Then it would print this is first and then unlocked a mutex It's currently unlocked. So unlocking and unlocked mutex does nothing And then the second thread is going to make it pass this lock call because it's currently unlocked And then it's just going to be stuck here forever because nothing else is ever going to call unlock So it kind of works. It kind of gets us part way there, but This also goes against spec. So technically You are only allowed if you look at the documentation to unlock a mutex Only from the thread that acquired it. So technically I shouldn't be allowed to do this either So one it doesn't work and two technically I'm not allowed to do it so Why we might use a mutex is say we had something like this So say my print first my print second were a bit more fragmented. So the system calls you can assume the system calls are atomic and Well, my print f-line if I don't read the documentation of print f This is a perfect perfectly good implementation of that print f. It could split it off into three system calls I don't know why it would but it might So if I try and execute this was it called safe print I Get like some crazy order sometime this I'm is going first second hmm I'm going for okay. So I got one complete line and then another complete line I got the opposite way around and then I got some gibberish again so Do we know how to make this always print full lines? So that it's nice and consistent Yeah, so if we do something like this we could have a Lock call and then an unlock call when it's done So this is what I would want my mutex for to make sure that between these two Critical sections well because they're between the lock and an unlock Only one can happen at a time and it will happen until that critical section is done So if print first goes first well, it would print all three of its lines until it unlocks and then the first thread can go print all of its lines and Or the first thread can go print all of its its lines and then the second thread can go and print all of its lines So in this case if I run it now I don't have any ordering, but I'm guaranteed that I get the full lines and That is something you need to check for whenever you use library functions now because they need to be What's called thread safe or have no data races? So I need to be safe to use with multiple threads in this case if my printf was implemented like that that would not be considered thread safe because it doesn't look atomic and it has some issues it doesn't print my complete line So when you actually start using threads in your real programs You're going to have to look at the library functions and make sure that they are Thread safe if you're using them with threads Luckily enough for you. Most of the standard C library functions are thread safe So like malloc and all those they're thread safe So you don't have to worry about it more of a case when you use other people's bad code And if they have not taken this course and you ask them if they're coded thread safe And they go huh, then don't use their code because it will probably be broken So do do all right. So let us go back So if we want to actually ensure ordering between threads we can use something called a center for which is basically just your idea of a flag So center fours are just a value. You can think of them as an unsigned integer. They're just a number that is always zero or greater and It just has two things you can do on it So you can call weight which will ink or weight which will decrement that value Atomically and then post which will increment that value atomically which means it'll happen all at once or not at all So you won't have any inconsistencies with it The only caveat here and what makes this useful is if you call weight It does not return it will block until the value of that center for is greater than zero So if the current value is of it is zero it cannot go negative So it will sit there and block until another thread increments that value so initially For center fours when you create one you can set an initial value to the center for to whatever you want Basically, you can think of it as the number of weight calls that may occur without any calls to post that don't block at all so Here is how you use it its API is really similar to p thread locks you have to initialize it By giving it the address there's this other argument p shared So this will change the behavior of what happens to the center for whenever you fork If this value is one it will be shared between processes So it will put the center for and shared memory So you can use it to ensure ordering between processes if you really want so after you fork that same center for is going to be In both processes. They're not going to be independent then the last argument is the Value to initialize that center for two after you're done. You should destroy it Then there is this weight call on it so again that will block if the value is zero and wait for it to become positive and There is also a call to try weight, which is the non-blocking version of it So it will just tell you whether or not it was successfully able to decrement that number Just in case you do not want to block Usually you do want to block though and then for post that just increments So whenever you see post think of that as an increment weight That's a decrement and it'll wait around while that value is zero So here is our problem again So let's see if we can fix it So now we know the magic of a center for so let's get rid of this horrible lock and this code that is awful So if I want to use a center for well, I have to use the library And then I just declare one I will use my imagination and call it SEM Here I Will initialize it so SEM and then SEM I don't want it shared between processes and initial value What should I set the initial value of it to be? hmm Let's throw one in there for now and see where we go So if we want The second thread to always essentially wait for the first one to finish Well, we should probably have a SEM weight here because in the case that the second thread goes first we want it to block until the first thread has printed its line So I would want a SEM weight here So with my current initial value of one will this work? Probably not what should I set the initial value to be? Big ol goose egg right so I should set the initial value to zero and now I have half of the equation so if the initial value is zero if The second thread gets scheduled first it would hit the SEM weight and Because the current value of the center for is zero it's going to block here and it cannot proceed so this thread can't do anything and eventually print first will execute and Hopefully print this is first and then in this case right now it can die and then The second thread execute continues execution, but it's still blocked because it's still in this weight So what am I missing? Yeah A SEM post where should I put it? After this right SEM post So now if I do this well I have to start arguing what happens if thread one executes first and then thread two and what happens if thread two Executes first and then thread one So in this case if thread one executes first well they'll print this is first which is good and Then do a post on that center for so it will change its value from a zero to a one So now its value is one Eventually will execute the second thread it will hit weight here Which is fine. So the current value is one So it will just decrement it from one to zero and keep on going and then it will print I'm going second and That's it Now the other case if the second thread goes first while the initial value is zero So if it hits SEM weight the current value is zero. So it's going to block here can't make any progress Eventually will context switch over to the first thread It will print this is first and then post that value from a zero to a one Then eventually whenever we context switch back to thread two. Well, it can now progress past the weight Because it would decrement it from a one to a zero print. I'm going second and in this case Doesn't matter what scheduling the operating system does I should see the same result both times So I should see this is first. This is first. This is first every single time I execute this and The benefit of this like I could put a locker on the whole thing I could have waited for one thread to finish and then another but the benefit of this is I'm only waiting for the ordering of the print F's and This comment part that says this takes a thousand seconds that could run in parallel Nothing stopping them right now if we're running in parallel I only wait as long as I need to to get the proper ordering and then after that everything runs in parallel and we're all good So any questions about that? Yeah Yeah, if I put try weight here and I don't check the value and I don't do anything about it It does nothing because it could have failed or whatever Yeah, so the purpose of using try way is like the same reason you might want to use try try lock because Well, if I try weight and it fails Well, I know I can't do this, but maybe I want to do something else Maybe something there is something else safe to do and that depends on the program But in this case, there's nothing else I can do except print. So I have to wait anyways So I may as well use the blocking one, but sometimes yeah, if you really want to eke out the most performances you can you can like Try lock or try weight in this case And then if you don't do it Just make sure you don't print you can do something else useful though. All right, so All right, so here is our change to make sure it always executes print first then print second So Like I said, doesn't matter what order the operating system Tries to execute the thread and will always get the same result So here's another question to you. This looks very familiar to a mutex So can I use a center for instead of a mutex? If so, how do I do that? Yeah, so Yeah to use it as a mutex basically I could set the initial value to one like I was kind of doing for my locks except This is a bit reversed because I'm Decrementing instead of incrementing just because that's how center for works So I can set the initial value to one and then instead of walk I can call weight. So I Can decrement it from one to zero then if someone else calls weight they get blocked So they can't progress I would have mutual exclusion and then instead of unlock I just post so I change the value from a zero to a one and then another thread could wait acquire the lock still have mutual exclusion so If I wanted to I can their equivalent Mutex is basically just a special case of a center for so if I wanted to make my That count example work I could initialize that mutex or sorry that center for with the value of one and then instead of lock I call weight and then instead of unlock. I call post Works exactly the same but a seven four is just more flexible, right, so Ideally if a mutex is What you want so if you want mutual exclusion just make your intention clear and just use a mutex Even though a center for is you know, it's just a special case of a center for but Especially once you get into data races and stuff. It's just about clarity. Just use the simplest thing you can That solves your problem. Don't go any further than that Typically, we just use center for is for signaling and making sure that there is a proper order So speaking of proper order, let's do a very complicated one So here is our situation. This is a producer consumer problem that we've probably heard of before Kind of half nods so We're going to do this with multiple threads So assume we have a circular buffer, which is just a fancy word for saying it's an array that if we make it to the end It just loops back to the beginning So if my array has n elements in it Well, I have indexes 0 up to n minus 1 and then if I'm at the last index and I try to access the next element It should just loop back to the beginning and access element 0 So I will have essentially two pools of threads I will have a pool of producers and a pool of consumers so each of these slots in the array will represent some data that is Written to by the producers so the producers write data to the buffer and then the consumers read data from the buffer and then process it So producer does some work to come up with some data writes it to the buffer and then All of our consumer threads trying to read that data from the buffer and then do some calculation on it Machine learning is a thing. It's doing machine learning. Yay So we have two rules here The producer threads they should not overwrite slots So if I just have producers and they fill up all of these slots here They should not overwrite slots. So in this case, I have one two three four five six seven Elements in my array. So as soon as all the producers fill up those seven elements They should wait before trying to fill another one until the consumers actually empty a slot because I don't want to essentially Overwrite data and then they can't access it anymore For my consumers, I should make sure that it doesn't read any data that has not been set by a producer So it just shouldn't read empty values so I Made this problem a bit simpler so I got rid of all the data races so all of the producers will start at index zero and then increment it sequentially and There won't be any data races between them So they'll always get it in the same order and the consumers also operate in the same order So they will start emptying at slot zero then go on and on and then they'll double back on themselves so All the producers share an index all the consumers share an index and there's no data races between them because I wrote them and you trust me so with that we still have the problem of Overwriting slots that are already full and then emptying slots that have nothing in them So let us look at our fun example So here is the code so we have a buffer size of 10 elements We'll probably want some center for us. I'll get rid of a compiler warning and then in here So ignore the while loop I essentially just use a center for so I produce a set number of elements and I consume a set number of elements So don't worry about the while loop So in a real program that are just using pools that would just be like a while true But I want to bound it so my program actually ends The only thing that matters in this case is the body of the loop So in here, I Simulate doing some work. So this is my producer. So it's going to have to do something to come up with the data Get it from a website or something like that and then it's going to fill a slot And we'll have as many producer threads as we can make Then the consumer threads will essentially do the opposite thing So again, don't have to worry about this while loop just care about what's in the body So each consumer thread just while true will try to empty a slot and then we'll just simulate some it doing some work So read some value needs to do some computation on it So if we run this I Set it up that you can change all the arguments if you really want the ones We really care about the first number is the number of producer threads We have and then the second is the number of consumer threads we have So if I have ten producers and ten consumers I can run that and I have it set that any time it does One of the things we don't want it to do it will print it off in a red line So here initially we can see that the consumer runs it empties slot zero, which is already empty We have not put any data in it. So that's an error keeps on doing this So it empties that slot empties that slot empties that slot So it does it ten times in a row and then finally the consumers or the producers run So now they fill the slots which don't generate any errors because that array is initially empty So it fills slot one two up to nine and now our Consumers empty some slots So it empties five and then we fill them back up starting at slot zero, which is fine and we eventually end so We could do a different number of producers and consumers and see that This case we are emptying slots that are already empty. That's not great. Then we fill all of the slots our consumer threads start emptying and then we start filling again and If we start filling again, we just start filling at slot zero, which is already filled nothing has actually Consumed that yet Then we go on slot already filled slot already filled and then eventually it works. So how would we solve both of these issues? so it's usually easy to Start this and break it into multiple parts. So one of the first issues is We might have a consumer thread that runs immediately the consumer thread runs immediately Initially my empty is all empty. So you you don't want it to empty anything if I haven't produced any data, right? so in that case It's typically easiest with this to place your weights first So the first weight I want is probably in the consumer. What should the consumer? When should the consumer weight before empty, right? So it should wait here hmm Need a name So we can call one seven four. I'll give you a good name for it to start you off We'll have it keep track of the number of filled slots. So this means that it will wait for a slot to be filled So initially, there's no slots. There's no slots filled So let's create the center for Then I'll initialize it So initially, what's the value? How many filled slots are there? Zero initially, right? So that makes sense so now if I set that value initially to zero and My consumer Happens to run first it will hit this weight and it blocks So it won't empty a slot that is already empty at least initially which is good But if I run this as it is right now Probably going to be a bad time So it just kind of hangs and you can see that all that all the producer does It's just fill all the slots and it just comes back around and starts filling more slots and No consumers run. So how do I fix? No consumers running? Yeah, I should probably post in the producer Probably after I fill a slot, right? So I will post filled slots So every time I fill a slot. I essentially increase the value of the center four by one and I let one new consumer pass So is that good problem solved? I'm done No What should I fix next? Yeah, so the producer should only fill empty slots, right? Okay, so that case I should probably place my weight first So if I place my weight first well, I want to wait in the producer So I want to wait on What what should I call this? Empty slots, that's a good name So let's create a new center for all right. What should it should its initial value be zero? Yeah buffer size right so initially each of my Consumer threads could fill or sorry my producer threads could fill up to buffer size because I can fill that whole thing It can only handle However, my slots we have so buffer sizes 10 can only fill up to 10 things So here I am waiting on an empty slot initially I have 10 of them so I could run 10 of these threads before consumer runs All right. Am I good now? Well, I know I need Yeah, after I empty I will post The empty slots There good All right, so most of the time I got this problem when I had 10 Producers and three consumers Which case I execute this I got no red lines. It's pretty cool And let's see how it's working so We can see the ordering so I fill slot zero. That's fine. I fill slot one That's okay, and then my consumers come in immediately empty slot zero and one Then I fill slot two. I empty slot two immediately Then I fill up slot three four five six seven eight Nine and I can fill up slot zero. That's fine because it's already been consumed So I can overwrite it. I can overwrite slot one. That was emptied already, too And then I can overwrite slot two that was already emptied and now Emptying just resumes so it starts emptying slot three and I fill slot three as soon as it's done Then it's empty slot four or five. So I fill slot four it empties a bunch of slots now and Empties the remaining slots because there's nothing else to fill so anytime I run this now I Get different I can get different orders of stuff I can even have more producers and consumers and when I execute this well since I have more Producers and consumers I Should see whenever I fill a slot eventually it will just immediately get empty so I fill it empty it fill it Empty it empty it fill it empty it fill it empty it fill it empty it because I have more consumers than I actually have producers so Fun, huh? Cool. Any questions about that? That was a fairly complicated example, and we got it So if you wrote that so you can write that in your own program make it go go fast because the idea behind this is All my producers and all my consumers since they properly synchronized and they're in order while This main part the simulate doing some work could all be done in parallel, and you get lots of speed ups All right, we're good All right, cool. We can wrap it up and Oh, I'll ask you one question. Make sure you're not asleep So yeah, here was our solution so making sure producers never overwrite filled slots Well, we put a weight. Well, this one goes in the other order So we waited for empty slots initialize it to buffer size So we put a weight in the producer then post in the consumer Then we ensured that consumers never consume empty slots. So generally it's easy to do this once at a time Initially, there are zero empty slots. So that was our initial value. So that our consumer waited and That was our solution So now my question to you. What happens if I initialize both the center for values to zero? Yeah Yeah, it doesn't do anything If I initialize them both to zero. Well, if a producer thread runs it hits some weight Can't make any progress yet And then the only other choice I have is consumer threads Which is called set weight and the current value zero So nothing can do anything. So that is called a deadlock and we get to discuss that next lecture So what we saw today we use center for us to ensure proper ordering between threads Before we saw a mutual exclusion. Now we can do even more complicated things. So initial center for us Pretty easy. They're just basically an unsigned integer They just have a value you can set the initial value to whatever you would like They have two operations that are atomic So they don't have data races post will increment that value And then weight will decrement that value and if the current value is zero It will block until it can successfully decrement it So with this well, I guess what you still need to prevent data races. This is just to ensure ordering So just remember phone for you. We're holding this together