 All right, welcome back to Operating System. So yeah, final exam review. What shall we do? It is up to you. Any request other than that, I will start flipping and you can stop me. The compare and swap to just implement a spin lock or something like that. All right, let's see how much I remember off the top of my head. All right, so, whoops, that's not for you to see. All right, so compare and swap takes a pointer to an int, some old value. So that's what compare and swap looks like and this is an atomic function. So it either all happens at once or it doesn't. And what it will do is load the value that is at this location and then see if it is equal to this old value. If it is, it will swap that atomically and swap the value and put in new value and it always returns whatever it currently read. So like atomically, it would do essentially current value equals, you know, dereference P and then if old val, whoops, that's like Yoda conditions. Check if current is equal to old val then P equals new val return, actually it's gonna return current anyways. So that's essentially what compare and swap does and that all happens atomically. So if you make your lock, just your lock function, something like this lock, it's just a pointer. Whoops, I should call it mutex, whatever. So you could have while compare and swap M01 like that. So that could be my lock function because all it's going to do is try to atomically change the value one to, or sorry, change the value zero to a one. And if it's able to do that, then it will return zero and it'll break me all that while loop. So if I break out that while loop, I know I atomically change the value from a zero to a one. And that's the only thing I need because after the lock, I should be the only one that's actually reading this location that can, or writing to this location that can change it. So if I want to unlock the mutex, I just change it from a one back to a zero. We need the compare and swap so we can atomically change this value and make sure that only one thread can change this value. Because if two threads are fighting for it, well, if they both call it at the same time because it's atomic, only one of the threads will actually change it from a zero to a one and not both. Because before we had the implementation where we just checked over and over again and then like we read it and then we could get context switched out, they could read a zero and then they both write to it so they both pass by it. So does that answer your question? So it will always continuously try over and over again and it will exit this function if it finally changed it from a zero to a one. It will only do that once. Yes, yeah, if it does it, it means you have the lock. I'm the thread that changed it to a one. So it is my lock now. Okay, yep, sorry. Yeah, so if it successfully changes it from zero to one, then that function returned zero and then because while zero, it actually breaks out of it and for anything else, it returned one and then it just keep on trying over and over again. Okay, anything else or should we start flipping? Like this, flip. The implementation with the guard and the lock, not that much, I guess, but the guard was essentially a spin lock, right? If I, yeah, yeah, so has mutual exclusion, no data races. You'll get some data race question, I guarantee it. Probably not, probably, yeah, wouldn't be like describe how this lock works. If there would be, it's like, oh, when should I use a spin lock for some mutex? That's valid. Yeah, all right. So short answer, skip this. So buddy allocator, we kind of just went over that. Explain the purpose of the condition variables, concept of RAID 5, virtual machines that won't be on it for you. The kernel mode stuff, I think you had a question like that on your midterm. Sir, by the way, virtual machines won't be on the final. No virtual machines for you, you are. All right, it is not mutual exclusion. The final will have processes on it because I can't write code without it being a process if I run it. And it'll probably have virtual memory on it because everyone likes virtual memory. And it's a, yeah, so stuff I put on the final is generally stuff that I want to make sure you know. And the scheduling question average was super high so I don't need to put it on it. So page replacement, you will probably get a question on that because I haven't tested you on page replacement yet. So, should I do page replacement? Probably not, probably a waste of time. Hopefully everyone knows page replacement. So just practice it if you haven't. All right, virtual memory. You should probably do locking. So let's skip unless someone yells at me to stop. So virtual memory, processes, processes and threads. I think we kind of reviewed that one. Let's do locking first. Everyone locking? All right, locking, sweet. So consider the following code. We have three threads each executing their own function here. That's what the setup will be. And in thread one, it locks mutex one, then mutex two. In thread two, it locks mutex two, then mutex three. In thread three, it locks mutex three, then mutex one. So if I have three threads each running their own function here, question numero uno. It doesn't even ask if there will be a deadlock. It says explain a deadlock. So give me the deadlock condition here. Yeah, so specifically, what's the deadlock condition here? Yeah, yeah, neither can make progress, but what is the situation with these three threads that you can give me where no threads can make progress anymore? So they have to execute the instructions from the top to the bottom. So do you mean thread one acquires mutex one and then thread two acquires mutex two and then thread three acquires mutex three and then we're screwed? Yeah, so is that the condition everyone wants to agree on? So if thread one, so thread one, it locks M one, then thread two executes and it locks M two, then thread three executes and it locks M three. So if we are at this point, now this is our condition with a deadlock because well, thread one, it is going to try and execute this line, tries to lock mutex two, but it can't get mutex two because thread two has it. Thread two wants to execute this line. So it tries to get mutex three, but thread three has it and then in thread three, it's trying to execute this line to try and acquire mutex one and it can't do it because thread one has it. So none of these threads can make progress anymore. Therefore, they are all deadlocked. So good. All right, fix it. The order of mutex one and mutex three, where? In thread three. All right, so just swap these two lines. Well, that... And the ones where they unlock. And these ones. All right, will that work? Anyone with an argument as to why this would work? And specifically what is the name of the deadlock condition we are eliminating for bonus points? So we can verify that this kind of works. So if we had the case where thread one executed then thread two executed and then thread three executed, if we swap the lines, thread three can't make any progress because essentially we'd be trying to acquire lock one. So it can't make any progress, but the other threads could make progress and doesn't really matter. One's going to have to execute at some point. If T one executes, well, it would lock M two. Well, it wouldn't lock M two. So we actually only have one thread that can execute and it is T two. It would lock M three and then it would do whatever it needs to do. And then it is still able to run and then eventually it will unlock them both. And then at that point, thread one would lock M two and eventually it would execute and then unlock both of them and then finally thread three could run. So it seems to at least alleviated that scenario. So anyone for explaining what the condition is we're eliminating here or does this work? It seems to kind of work. So why does this work? Explain why this works? Yeah, why this fix prevents any deadlocks? Yeah, it breaks a circular weight condition because every thread acquires the mutexes in the same order. So we always do in order one, two, three. So if we always acquire them in the same order in all threads we break circular weight. Therefore we do not have a deadlock condition. What's another way to fix this? You have to use two mutexes. Yeah, yeah, the try lock one where I try. If I don't get it, I give it up, yield and try and get it or then get again, try again. What deadlock condition am I breaking if I do the try lock example or try lock solution? There's a special name for it. Yeah, no, not preemption because preemption is just I take a lock away. We didn't have that as a solution for if we have the try lock solution here where if we don't get the other one, we give it up. All right, it's just called hold and wait. Hold and wait is the solution. So either those are the two main solutions we have. So either we acquire them in the same order break circular weight or do the try lock and we get rid of hold and wait. All right, yeah, that was it for the question. So explain the deadlock condition and propose a solution. So if you just swap two lines, easy as six, hopefully it should be the easiest six marks you got if you could explain it. Just had to swap two lines. So, center fours. So here I even hid the solutions. So this is very pro level hiding. So we have this following code. We have eight total threads. We have a semaphore. We have an initialized semaphunction where the value is given as a blank. And in a run function, we are told that there is this initialize everything function that we only want thread zero to execute. And then we want every single thread or core to execute its own initialized thread function only after, and I repeat, only after initialize everything has run. So, assume we have n threads set up to start executing the run function. Each thread receives a unique thread ID. In addition, the initialize semaphunction executes before any threads get created so it's safe to initialize the n4. So, question one for this part is using a single center four enforce the synchronization constraints specified in the comments. You may add code or pseudo code or explain it or do whatever. Try to make a solution that supports any number of threads, assuming thread zero always exists. For a maximum of seven points, you may cheat a little bit and use that total threads and assume it's the actual total number of threads executing the run function. So, how would I use the center four to make all of these comments true? I want initialize everything the run only in thread zero and then before any other thread calls initialize thread. So, first step is see basics. How do I make only thread zero execute this initialize everything function? Yeah, if thread ID is zero, so, boo. All right, part one. What about for the rest of it? Yeah, we just have one semaphore. So, usually what is the easiest thing to place first? Okay, wait, first. Does everyone remember what a center four is? Yeah, semaphores are basically just an integer value and I get to use two atomic operations on them post, which will atomically increment it and then wait, which will atomically decrement it if it will become a non-negative number. So, if the current value of the center four is zero, it will block until another thread goes ahead and posts it back up. So, it can actually decrement it without it going below zero. So, do we remember what function is usually the easiest to place first? The wait first. All right, what do I want to wait? Well, where should I place my weight? After initialize everything. So, let's assume I just have brackets here, so you want a sem wait here, outside the if like here. So, we want only thread zero to execute this function and then after thread zero executes this function, any threads can execute this. Yeah, yeah. What do you mean by you let thread zero take the lock? Oh no, it take the center four means. Increments it atomically, increments the value. Not quite any other suggestions? So, so it's value two one and then, so where do I want? Tell me what else should write? I wrote the value is one. Value is one. All right, now we're setting it to zero. How do I make all the other threads wait before doing anything? Like wait here? Nothing. Okay, let's start again. So, the way to think of this is without setting the initial value or anything, usually the first thing to place is the sem wait function. So, in this case, what do I want to wait? I want all the other threads that are not thread zero to wait until thread zero calls this initialize everything. So, in that case, just to be explicit about it, I could have an else and then in the else, I will have my sem wait. So, now is when it's probably appropriate to think of our initial value because that could execute first. So, right now, if the initial value is one, that is probably bad because it could be the case that thread, I don't know, three executes this function first and then, well, it's not thread zero. So, it goes into this else and then it does a wait. If the current value is one, it will change the one to zero and just keep on going and then thread three calls initialize thread and that happens before initialize everything. So, that is bad. So, that probably means that our initial value should be zero. So, in this case, so we have a post here. You can call it post multiple times. Yeah, so, just like four num threads. The, the, the num threads. So that, so that should work. So, if thread three or thread four or whatever makes it here, then it will wait. The current value is zero. None of them can progress and then only thread zero can execute. It will call initialize everything and then post that center four value up all the way up to like seven or eight and then each of the threads can go ahead and decrement the value and now they can all execute their initialize thread function and we're all good. So, that was the solution for seven marks. So, now it's your idea. It was just kind of hard to follow or the process of getting there. You got to the correct solution but the process probably confused a bunch of people. So, if I wanted to just have one center four, I don't use the number of num threads. What I can do is I can place a post here. Yeah. So, in this case, I can put a post there and what that will do is, well, if any of the threads, you know, one all the way to seven, say, try to execute this first, well, they're all going to be stuck on the weight because the current value is zero. Then the only way to change that value from a zero to a one is after thread zero executes, it would post it back up. So, it would change from a one, or sorry, a zero to a one and then some thread could wake up. That was on the weight. Let's say it was, I don't know, thread three and then it could get through the weight. So, it would change the value from a one to a zero and then we would have it post immediately after to wake up another thread. So, another thread can execute. So, it would change the value from a zero to a one and that way we could have, you know, thread three or thread two execute. It would do the exact same thing. And essentially, each time through a thread is going to wake up another thread until they're all finally through that same weight and they can all call initialized thread and we're all happy and that works with any number of threads. So, questions about that one? All right. Exactly that. Yeah, I guess we're the last one. So, yeah, the initial value is zero. Yeah, it's just confusing. I think he confused everyone. Yeah, yeah, technically you can do that as well and don't have an else. It's just slightly inefficient because thread zero doesn't need to do that. Yeah. So, if the weight actually acquires this number four. Weight just decrements it. Weight atomically decrements the value. If the current value is zero, it will wait until it becomes a positive value so it can decrement it without going negative. All right. Oh, here we go. Considering the same code. So, here is a solution without center fours. So, we have if thread ID is zero initialize everything, otherwise yield. So, assume we have cooperative user threads. So, this would be like your lab four. So, assuming I have your lab four implementation, is this a correct solution? If we set it up, that every thread can go ahead and execute this. Yeah, so we got one yes, anyone else? We got two yeses, everyone else peer pressure? Okay, so the answer is yes. Assuming we have a FIFO queue and all the threads are just in the queue. So, assuming we have a FIFO queue where if you yield you just go to the back of the line, while it doesn't matter where thread zero is in the line, essentially every thread is going to be shoved to the back and then whenever it's thread zero's turn, it is not going to go the back, it's going to execute first. And because they're all user threads, you have control over the scheduling so you know exactly what is going to happen. So, this would be a valid solution. All right, other, whoops, didn't see that. All right, so now we have the exact same thing except now we have cooperative kernel threads, running on a machine with multiple cores. Now is the solution correct if we have kernel threads? Yeah, so what is the difference between kernel threads and user threads in terms of usability? Sorry? Yeah, kernel threads can run in parallel and also do I have control over the scheduling of kernel threads? Nope, so if this was our solution and they were kernel threads, this would not be a valid solution because well, even some silly case where like you have core one and core two, well, I could have thread one on one, thread two on another and then if these are the only threads on the core, whenever thread one calls thread yield, well, the scheduler could be like, well, there's nothing else to run on that core anyway, so I'll just continue executing thread zero, just kind of ignores the call, goes on, continues. They could execute in parallel. Thread zero could just be scheduled after all the other ones. You don't know, they're kernel threads. So this solution will not work if they are kernel threads since you essentially have no control over it. Yeah? So basically we go by the kernel and we can assume that it may run in a really weird way. Yeah, kernel threads are controlled by the kernel, so the kernel knows about them, it schedules threads independent of any other thread. And we don't always... Nope, the kernel can schedule it however it wants. Uh-huh, thank you. Yeah. All right, well, this is a good question someone else brought up. All right, fix this but with condition variables then. Do you wanna do that? What's a condition variable? Means we should probably know what that is. So, condition variables are essentially just like cues, how they work, right? And then there is a signal and a weight where weight will block until another thread signals you and wakes you up. So do you wanna try this with condition variables or go to the next one? You should probably know how condition variables work on the final. Am I just saying that? I don't... Yes? Yeah, it could just be the same thing except if it's the condition variable, we could have some boolean flag, bool is init, shellize, something like that. And then some con variable, let's call it conned. Huh? You can include standard booles. Yeah, so in that case, well, we also need a mutex. I think you still have to include that header but I think that header is defined in C99. All right, so if we have this so with the condition variable we need a mutex because while it needs to be protected we have the condition variable and the is initialized. So in this case, we would have to lock the mutex then call initialize everything and then we can set is initialized equal to true and then unlock and at this point, well, we would probably want to signal a bunch of times. So in this code, instead of signaling up to, I knew I said I wouldn't test this but in this case, it would be kind of wasteful so there should actually just be a broadcast here which will just wake up every single thread. So instead of a broadcast, I could just have a for loop that says for one to power may threads there are call signal that many times because there might be that many threads waiting. So you can assume your question won't look exactly like this because broadcast makes more sense in this case but broadcast would wake up every thread. You do not have access to the queue itself. You'd have signal in broadcast. Nope. So then if we have our, so broadcast on the condition and then in the else would probably look like, what does it look like? Lock the mutex and then while not is, yeah, then I, yeah, I conned wait. I'll just shorten it. So wait with conned and the mutex and then here I could call unlock and that would be it. So now in this case, so grabs the mutex it will check if it is true. If it is currently false, it will wait. So that blocks this thread puts it on the queue and also unlocks that mutex and then whenever it gets woken back up by signal or in this case broadcast, well it will reacquire the mutex and then check again and either put self back on the queue or it will actually, or that condition is actually going to be true now and then it would unlock and then we can go ahead and start executing the rest of the function. So in this scenario, would it be a good idea if I change that while to an if that is a common trick? Yeah, so this example probably okay because it just changes once but in general it should be a while. And I like the general case because if it's a while then it works in the condition then if would work too, but always be a while. All right, 10 more minutes. So any other requests or just flip? All right, yeah, yeah. If it's kernel threads, you just have no control over it whatsoever. So they could run in parallel. You just thread zero just couldn't be scheduled until way after the other ones. Even if they yield, you have no guarantees of the kernel threads. All right, threads. Oh, this looks fun. All right, let's do this quick. So this is a super fun threads one. So let's read the code super quick. So we have a global here called array that has an array size. Then we have a global variable called global sum and then a run function which gets a thread ID and then creates a start and an index which doesn't really matter in this case. And then we'll have a variable called local sum. It will increment it and then increment global sum. And in the main, we create num threads, create them all and then join them all and then we print off a global sum. So each thread is calculating its own sum of this array and then joining them all together at the end. So can we see all that more or less? So this is the main one. So, is there a data race with the variable called array? Not changing. All right, so the arg after thread ID is here, it's just a unique location for each thread. So yeah, so for array, who remembers exactly what the definition of a data race is? Yeah, two concurrent accesses where at least one is a right to the same location. So in this case, do we have concurrent accesses to the array location? Yes. Yeah, actually no we don't. Okay, so we don't, but even if we did for some reason, we don't have a data race with array because it's only read, it's never written to. So if we only read from it, not a data race. In this case, they're actually all disjoint, but even if they weren't, then we don't have a data race with array. All right, do we have a data race with the local sum variable? No, it's local. To each thread. Yeah, for each thread. So we don't have a data race for local thread because well, each thread has its own stack and local variable, whatever I call it, local sum will be a variable on its stack. So I do read and write to it right here, but I don't have to concurrent accesses to it because it is unique for each thread. So there's no data race with local sum. Is there a data race with global sum? Probably yes. Probably yes, so probably yes is you will probably get a mark on it. Yeah, so there is a data race on global sum. So this location is accessible in every single thread and we are reading and writing to it concurrently. So we may have a data race. So yes, how would I fix it? Yeah, so just add a mutex and then I could just do a lock. Does that fix it? Yeah, assuming M, I only define one M, like we have M up here that's initialized, whatever. So that probably won't get you a grade, but there, close enough. Yeah, so a question eventually. So is it necessary to protect access to array? No, protect local sum? No, protect global variable? Yes, how would I do it? Several solutions, easiest that I assume most of you did would just mutex around it. If you said make an atomic int, then whatever. I gave you the grade even though we didn't really talk about them. Yeah, well then you'd say no, they don't, they access different elements. That's fine. Well, if they only access their own element, doesn't matter if it reads or writes, if it's unique, then even if it's reading or writing or reading, whatever, it's fine, right? All right, last one was, oh, if the threads were changed from joinable threads to detached threads, would this lead to any issues? Anyone help? What do detached threads mean? Yeah, they just, whatever they're done executing, they're just, so detached threads mean I cannot join them, so if I had detached, essentially that line is gone and I have a detach, so what issues would that lead to? Yeah, so what's likely going to happen is, well, I just create all the threads and the main thread just continues on executing. It prints global sum, global sum is equal to zero. It returns from main, which is the same as calling exit, process is dead, all the threads are dead, so nothing actually happens. Or it could also be the case because you don't know how they're being scheduled where one thread adds to global sum and then we get a partial result here and then the process dies or two go and we get a partial result and then it dies, what, so on and so forth. So any questions about that? All right, anything else for the last three minutes? Yeah, in this case, if they were detached and I wanted to print off the global sum, well, I can't. I need them to be joinable because I need to make sure that they're all done. There is a way to make sure the process didn't die until all the threads were done. So if I ran pthread exit here instead of letting it exit, then my main thread would exit and all the other ones would eventually finish, but it doesn't really matter in this case because sure the global sum is eventually right at the end but no one's ever gonna print it. So I'm never gonna see it anyway, so probably better off that the process dies anyways. All right, let's see if we can speedrun anything. Oh, there this, oh, won't have numbers for this but we can continue on with this file system question if you want, because guess what? You'll have file system one, so yay. All right, so if you have any other questions or anything you want me to cover, please post it in the section three discord channel and we'll do it, I guess tomorrow. We'll figure it out next time. All right, just remember, pull and pull ya. We're on this together.