 Alrighty, welcome back to Operating System, so today we get to talk about locking. So there are some things called monitors that are built into some languages because with object-oriented programming, some developers thought mutexes were too difficult to use so they wanted something easier. So what you can do with a monitor is you can mark a method as monitored and let the compiler handle the locking for you, that way that essentially it just creates a mutex for an object and then an object can only have one thread active across all of its monitored methods so it makes it essentially just does lock and unlock in every method even for and then make sure before every return statement it actually unlocks it. So basically like I said before creates one mutex per object and then the compiler will insert the calls to lock and unlock for you. So in Java there's the synchronized keyword and that's an example of a monitor so in Java if I wanted to create a bank account or something like that well I could create an account, have an int balance and then if multiple threads are calling deposit and withdrawal and updating the balance well banks probably don't want a data race with your money so I can mark both these methods as synchronized and what the compiler will do is it will create essentially a mutex called monitor and in the deposit method it will add a lock call to the mutex at the beginning and then an unlock at the end and similarly with withdrawal it would do a lock with the same mutex and then an unlock with the same mutex again so it would make sure that without writing anything you prevent data races but the drawback is well you only get essentially one mutex per object and you might want to do something a bit more fine-grained for that if there are multiple variables that don't depend on each other you might not want to serialize the entire object but at least when I was learning about threads they just said add synchronize to every method and your code will work and yeah I had no idea what was going on so if you ever do this this is what's going on with Java with the synchronized keyword so there is another way to express ordering so before we saw centerfors another way to express the same thing that may or may not be clear depending on your preferences is something called condition variables and the way to think about condition variables is it essentially lets you have your own queue of threads that you can put to sleep and wake up so we'll see an example they have an init a destroy and then there are some functions that look a bit new to us so there is a signal and a broadcast and a weight so what the weight will do is a weight will essentially add this thread to the queue and then put itself to sleep with some other steps and then in order to resume executing that thread that's where signal and broadcast come in so what signal will do is wake up a single thread that is on that queue and then it can continue executing and broadcast will wake up every single thread that is in that queue so with that there is also a time wait if you want to you know you don't trust that someone will wake you up you can just put yourself to sleep for a set amount of time and then you'll get woken up after that amount of time so so with these condition variables they must be paired with a mutex so whenever you call wait you must already have locked the mutex so it needs to be you need to pass a lock call signal and broadcast do not need to hold the mutex when you call them doesn't matter they just wake a thread up so why do I need to have the mutex locked while I am calling wait well you can think of it as well since it's managing a queue it wouldn't want any data races using that queue so that might be another reason and also it needs that mutex argument because if we put this thread to sleep we don't want that mutex still held by that thread because it's not going to execute anymore so what it will do is it will also unlock the mutex right before it goes to sleep so and then whenever it returns back from sleep the first thing it will do is try and reacquire that mutex so when it returns from wait it has the mutex you still have mutual exclusion so when you're doing this one mutex can also protect multiple condition variables or queues if you want just as long as you are consistent so for this working we can only consider calls to wait and signal so wait puts the current thread on the queue puts itself to sleep and then signal wakes up a thread or in other words so wait has three steps so again wait will add itself to the queue for that condition variable easier to just think of the queue or the condition variable itself as a queue then it will also unlock the mutex and then get blocked so put itself to sleep it's no longer able to run anymore and then whenever the thread that called wait has another thread called signal or broadcast and then wakes it back up well it would remove itself from the queue it would be unblocked so it can is eligible to run again and then before returning from wait it will try and lock the mutex again so wait will only return when you have the mutex so you will have the mutex locked before and after you're called to wait so any questions about that yep yeah so the queue will be probably a FIFO order queue so it will just wake up whatever is in just wake up one thread that is at the front so if you want to wake up everything use broadcast if you want to wake up what is next in line just signal so what that would look like is well we could take our producer consumer example from last lecture and rewrite it using condition variables so instead of having two seven fours for keeping track of the number of filled slots and the number of empty slots well I can just keep track of the number of filled slots and then have essentially a queue that is waiting for a filled slot and have another queue that is waiting for an empty slot so in the producer I would acquire the mutex because my condition that I care about is the current value of the number of filled slots so I would lock and then I read then that number of filled slots don't have to worry about data race because I'm always reading or writing the number of filled slots while I have the mutex so in this case I will do while n filled is equal to n or capital n would be the buffer size so while all the slots are filled I can essentially put this threat this producer thread to sleep because it is waiting for an empty slot and then give it the mutex as part of this it would unlock the mutex so in the consumer while my condition is if the number of filled slots is currently equal to zero that means my consumer there's no data for my consumer to actually consume so I can wait for a filled slot so I'll add myself to the filled slot queue and then use the mutex so if there was a filled slot what the consumer would have done is empty the slot and then decrement the number of filled slots so it would go from you know two to one something like that so it updates it and then it can signal any producer threads that are waiting for an empty slot because it just filled one up so it would wake up one of the threads and then one of the producer threads would resume back from here acquire the mutex make sure that there's that all the slots aren't filled and then if that is true which it should be in this case it would fill a slot then say that oh well I've now filled a slot so I would increment that number and then if any consumers are waiting for a filled slot I would signal them wake up one of them and then unlock so any questions about that same example so wow that's annoying so this clearer than the center fours or worse so this is better so generally there's no one true answer it just kind of depends so some people like condition variables better than center fours for simple things sometimes center fours are better so let's make sure we understand this so let's go here so is there any issues with the following code so we'll just create say we have a global variable called condition and its current value is equal to false so I have two threads running and these are all the threads these are all the operations threads will do so because we're using condition variables at this line here in thread one we want to make sure no matter what all the code that is there should execute with the condition equal to true so it should not be possible to execute anything at that point while that condition is equal to false so given these two threads is there any way any interleaving of the threads that will cause that not to be true or something bad that can happen so thread two comes here sets the condition equal to true okay and then thread one executes so thread one executes it would acquire the lock no problem then check if the condition is false it is not so it would execute this line and conditions currently true so that's okay right conditions true so it wouldn't go into the while loop so let's reset subtle because I just realized I screwed it up in the last lecture so good thing you guys are second so what about if thread one executes first so thread one executes first it grabs the mutex checks the condition it's false so it would go into the while loop here right so what about if right before it puts itself in the queue well thread two executes it updates the condition from false to true and then it would continue executing do a signal right now there's nothing in that queue nothing's waiting so it wouldn't wake up any thread now we could context switch back over to thread one then it would execute this weight and essentially in that weight well it would put itself to sleep and add itself to the queue and now it's a sleep forever so it actually can't execute that line because nothing at this point is going to wake it up anymore so why is the mutex passed to the weight so as part of the weight so the implementation of this weight it has to add itself to the queue assuming that it didn't need a mutex to do that well before that thread goes to sleep it needs to unlock that mutex otherwise we couldn't progress in any other thread and when it wakes back up again it will try and acquire the mutex again so we still have mutual exclusion so is there any way i can fix this so that i will not have the case where thread one essentially gets stuck forever so let us reset things here i'll just write the queue for cond initially it is empty thread one wants to execute here thread two wants to execute here so how would i fix that case so why did that happen it was essentially a data race before right because thread two is modifying the value while thread one was trying to use it so if i want to fix that data race what should i add probably in thread two yeah probably called a lock right so here i could lock the mutex and then after i update it i can unlock it so now in that case well let's go through what would happen if we had that same ordering so what happened before was thread one acquired the mutex so we can say so let's just keep track of the mutex here so initially no thread has the mutex so we execute this line so thread one acquires the mutex so the mutex is held by thread one then it could check the condition the condition is currently false so it would go into the while loop and the problem before was we context switched over to thread two at this point and then we kind of got screwed so now if we context switch over to thread two its first thing is a lock and currently thread one has a lock so thread two cannot progress so we just prevented that from happening so thread two cannot run so thread one will continue running it will execute this weight and as part of the weight well what it will do is add thread one to the queue and also unlock the mutex so now the mutex is unlocked and thread one can no longer execute so the only choice we have is to execute thread two thread two calls lock in which case it can acquire the lock so the mutex is now held by thread two it could go ahead update that condition from false to true and then unlock the mutex so now no thread holds the mutex and then it would signal it would wake up thread one so thread one is eligible to run again and it will not return from the weight until it has the mutex so if it continues from the weight that means thread one has the mutex and then it would be executing this line with the mutex while the condition is true nothing else can change it to true we're all good and then whenever it's done it unlocks the mutex so any questions with that we're all good yep so the signal just removes thread one from the queue and it's eligible to run again because it's resuming from a weight if it gets picked to run it will essentially execute lock on the mutex so whenever it can acquire that mutex then it returns from weight so in this case here we can rewind so so right before we called unlock and thread two we also did signal so when we called unlock we essentially gave up the mutex and then when we signaled we essentially removed this from the queue and now it's eligible to run and it would resume from that this weight call right here and before it returns from weight it has to reacquire that mutex so if there was another thread that has the mutex it couldn't resume because essentially it would be doing a lock on that mutex so it would have to wait for another thread in this case it's the only other thread that can run so it would go ahead and just acquire the mutex and then it can it would continue executing from the weight so it would go back up check the condition again this case it's true and it would come back so you might also wonder would that be fine why the hell am i checking it again i knew it was true in this case it should be okay right because it only changes from false to true so it should be okay so in this case it is okay but in general it is not so here is a case where say we introduce a new thread called thread three so if we want to introduce a new thread called thread three and it updates the condition to false so here we added mutexes along everything that modifies the condition just to make sure there's no data races so if i change right here that while it has been changed to an if is there a problem in this code essentially what could happen at this line here so is it possible while executing this line here in thread one i can execute it while that condition is equal to false so i can so initially so is that possible to execute the light blue line while the condition is equal to false yeah okay so let's first thread one starts executing so it acquires the mutex so thread one has the mutex so it would check the condition it's currently equal to false so it would continue it would go to weight and weight would put thread one into the queue and then give up the lock so now what so thread two acquires the lock and in this case as soon as thread two acquires the lock we can't even switch to thread three because it wouldn't be able to make progress because it would need to acquire the lock so it continues executing there's no other choice and updates the condition to be equal to true and then next line gives up the lock and then it would signal so signal would essentially remove thread one from the queue and it is eligible to run again so now we can either run thread two or thread three so what do you are on thread three so currently the mutex is unlocked if we start executing thread three well it would acquire the mutex so it is now held by thread three thread one can't execute because while to return from weight it has to acquire the mutex which it currently doesn't have then we could update the condition to be false and then unlock the mutex and thread three so now the only thing to run is thread one before it returns from thread weight it would acquire the mutex so now thread one has the mutex and because this is an if statement it would resume and just go here and now it is currently executing that line while the condition is false which is not what we wanted so in this case in general you always want this to be a while while you check for the condition as long as the condition can change back and forth values you're not guaranteed that when you wake up and start executing again that it's going to be actually true so that's why you need a while loop here unless it's just changing once then in that case an if is fine but if you put a while there anyways it doesn't really hurt anything so in general just put a while loop so any questions about that all right because yeah I guarantee if you start using these and then you put an if there it'll work like most of the time then you make your application a bit more complicated where the value like goes back and forth and then suddenly one time out of a million something catastrophic happens and you have to debug it so just always put a while there that is my advice for the day all right so here's that example too so again we change the while to an if bad things happen so these condition variables they serve a similar purpose as center fours they actually kind of look like it so you can think of center fours as a special case of condition variables so if your condition that you're keeping track of with just an integer value then I mean it would be pretty easy to implement a center four for weight you just check the value if the value is currently zero you go to sleep and then in the post you increment that value and then you would signal after you have incremented it so you wake up something that is currently waiting so you can go the reverse way too so you can always implement one using the other that's like some computer science person has proved that but it can get really really messy and we are computer engineers and readability is important so for complex things just use whatever reads better so there's no true one way to verify that if a center four fits nicely and it makes sense use a center four if it looks like complete like a complete and utter mess probably means use a condition variable so it's something you will have to do use your judgment for we won't really play with them too much in this course we just need to understand how they work so next thing is locking granularity so that is the extent of your locks so we saw we need locks to prevent data races but if we just do a silly thing and put a lock around our entire program we turn the whole thing serial we don't have any data races but it's really slow so if we want good performance generally we should divide our program in smaller sections maybe divide the locks up so that you know if two things are independent they use different locks so they can happen in parallel and won't have any data races so for example the python interpreter is historically been really really slow really bad at threading because they just to prevent data races for them they just use one big lock around the whole python code so it was called the gill or global interpreter lock and that's why threading performance in python is atrocious although there's been work lately to actually remove that global interpreter lock and divide make a bunch of smaller locks so you can try and run more stuff in parallel so for example this will be a kind of an open question so if i by chance cough cough in lab five give you a hash table and ask you to paralyze it and make it go as fast as possible well you also want to prevent data races on it so there's a few strategies you can use you can try and lock the whole thing which case it'll probably be slow or you can try and divide up the locks and see if you can make some information independent so things to consider about locks as well they have overhead so they're not free it takes time to at least execute that lock function they take up some memory although they're not that much because they could just be a q and two ints another thing to consider about locks is contention so contention is essentially how many threads are calling lock at the same time how many threads are waiting so the more threads that are waiting on a mutex the more overhead you get the q is going to be bigger the q will have to grow dynamically and also all those threads are put to sleep they're not allowed to execute while they're waiting for the lock so contention is essentially just how many threads are fighting for a lock at any given time and then another thing is deadlocks so that's another new thing we need to consider once we have multiple locks so like i said the memory or the overhead for locks well they're going to take up some space in memory they might not be much but if for some reason you go super fine grain and you allocate a billion locks that's probably too many even if they're one by each a billion is still quite big if you always initialize them and destroy them while that takes time too so if you just create a new lock for one operation that's probably not a good thing and also the time to acquire and release locks aka lock and unlock them takes time the more locks you have the greater the cost is going to be so you have to find some happy medium so you do not want deadlocks that is the last thing we need to consider for locks thankfully so the more locks you have the more likely you will have a deadlock so there's four conditions for deadlocking one is mutual exclusion we'll only consider deadlocking essentially for mutexes so you have mutual exclusion because that's what they do by definition the second condition is hold and wait so you have to have a lock and try to acquire a different lock then the third condition is no preemption which means i just can't take the locks away so i can't if you're holding a lock and waiting for another one i can't essentially rob you of that lock and force you to put it down if i have deadlocks if i could force you to put it down then i won't have any deadlocks i can just essentially steal the lock from you the fourth condition is a circular weight so it's waiting for a lock held by another process so a simple deadlock example would be hey i have two threads i in thread one get lock one means i'm trying to lock lock one so in thread one i can lock lock one and then lock lock two and then in thread two i can lock lock two and then lock lock one so i might get in the situation where i execute thread one it acquires the first lock and then i contact switch over to thread two then thread two acquires lock two and now at this point i can't make progress with any thread so if i try to execute thread two it tries to lock the first lock which is held by thread one can't make progress and if i try and execute thread one can't make any progress because it's trying to acquire the lock that is held by thread two so that is a deadlock because neither thread can make progress so one way to prevent a data lock is to ensure a set order of the locks and that will prevent a data lock so of these four conditions here you just have to break one of these in order to prevent deadlocks so all these conditions must be true in order to have a deadlock if i can break one of them then i can break the deadlocks so this one prevents hold and wait because if i always acquire the locks in the exact same order between all the threads that means in this case if i only have two locks i can only ever get lock two if i already have lock one so i don't have hold and wait it's not possible to have lock two or it's not possible to yeah have lock two and try to acquire lock one because i always acquire them in this case if i just assume i always acquire them in sequential order it's not possible to essentially acquire a lower number so that's one way to prevent a deadlock and then the other way to prevent a deadlock is to break that hold and wait so this is finally where try lock becomes useful so if i can't have an or generally acquiring the mutexes in the same order is the easiest way to make sure i don't have a deadlock but for some reason you can't order the locks this is the next best one so what this will do is acquire lock one and then it has a while loop so it will try to acquire lock two and then it will check try lock will tell you whether or not you have successfully acquired the lock if you have not acquired lock two then you go into the while loop and then what you do is you give up lock one so i break hold and wait so while i have lock one i try and acquire lock two if i can't get it i immediately unlock lock one and then at this comment where it says wait at this point i currently do not have any locks so i could wait a set amount of time i could put myself to sleep i could yield i could do something it doesn't matter any other thread could make progress because i am now not holding a any locks then if i want to retry well i can try and acquire lock one again as soon as i have acquired lock one i would try go back into the while loop and try to acquire lock two again and then if i have successfully acquired it well then i can execute any protected code that needs to have both locks after that i can unlock them both so this won't deadlock because i don't have hold and wait so any questions about either way to prevent a deadlock cool all right so we explored some more advanced locking today we have another tool to ensure order between threads so condition variables are basically an explicit queue for some more complex condition signaling they're easier to use your opinion might vary though and then if we're using mutex's to ensure we don't have any data race the granularity of the locks or how much code they lock actually matters especially for performance because we're essentially making things run in serial and preventing any parallelism so once you get into that you will have multiple locks and then you have deadlocks which you must prevent easiest way to do that is to ensure an absolute order of acquiring the locks if you can't do that well you have to do that you have to do the thing where we broke hold and wait because we just try to acquire the lock if we don't get it we give up the lock so with that remember poem for you we're on this