 All righty, welcome back to our last 353. So after this, you'll be more or less done with me, except for the review session, which there's almost a tie between Friday and Saturday afternoon. Vote if you want, I don't care, up to you. So there was one vote to do threads in synchronization today as midterm review, so we can do that. So I can go, this is the final in fall for 344. Same course, pretty much. So here's the threads question. It is one of the questions I regret because I made a mistake on it. So it says, for the given multi-threaded program, determine if there is a specific order in which threads execute, explain possible sequence of threads that could be running at each key point in the program. Also, do you want me to give you a minute to think about it and then go up? All right, so there's the first question. There is the entire program. I will give you a minute to think about it and the corrections are in red, so use that. It makes it a lot easier and then we can discuss. So where's a timer? All right, any thoughts of this has to do with any particular thing? So thread one's going to print first because thread zero waits on. Yeah, so there's gonna be a specific order, at least how it's written right now because, well, let's see, I'll do it in blue. We have our first main thread running. It would set a global variable to one, then create thread one. Now at this point, we don't know which of these two could run. So could be the case where thread one runs all the way to completion. Could be the case where main goes next. We don't know. In this case, nothing's stopping thread one from finishing right now. It could just go right to the end before we even create thread zero. So no specific order so far. Then main creates thread zero. So that would start off like that. And now any of these could run, but well, if the main happens to run, it's not going to get that far. It's just going to make it to join and it can't make any progress until thread zero is done. And then thread zero, well, it can set its own local variable to three and then join and now at this case, we have no choice but to execute thread one, right? So there is a specific order. We have to execute thread one first. It has to complete first before, so thread one has to complete first before thread zero passes line six. So thread zero cannot pass this line before thread one's done and also the main thread cannot pass this line well before thread zero is done as well. So no choice but to actually execute thread one in this case, right? So is it defined order? Thread one has to finish first. So it would create, it has int y, so int y local variable to thread one, then y is equal to two. It would increment the global variable x. So here I'll write out what x should be. So the main thread updated x to be one whenever it first ran. And then now we increment x by two, so we should update the value to three. Then, well, we've already done the first question, right? We just said what the order ism, they have to run in this order. So now we're doing the next question that says, identify all possible outputs of this program may generate when executed. So right now we can only execute thread one, so we're doing that. So now thread one would print off thread one, x equals three, y equals two, and that's it, right? Then thread one is done, and now since thread one is done, the only thread that can run now is thread zero, because it can now return from the join because thread one is now done, right? So now thread zero is the only thing that can go. It increments the global variable x plus equals y, which it's y is three, so we increment it to be a six. Then it prints off thread zero, x equals six, y equals three, and then it's done, then if it's done, the main can return from the join, then we would print main, and then the global variable x is still equal to six, right? All right, that's how I intended the question to be done, but as you see, I did some corrections because I wrote it like this, I'm like 99% sure, and then I revised it because I looked at them like, you know what, having it say one and zero looks a bit silly to me, I think it would read better if it was zero than one. So I just did this and then gave that as a question. So this makes it infinitely harder. Anyone want to explain what happens if I do something silly like that? Yeah, yeah, I could join a thread that doesn't exist. So I could create thread zero, so here, let's rewind all the way back. So I start with my main thread again. I update x to be one. So now instead of zero, it's a one. Then I create thread one, or sorry, thread zero. And if I create thread zero now, I don't know which one executes. So it could be the case that thread zero executes. So it would create its own local variable y, which is fine, but then it would join thread one which doesn't exist. So it would just get an error. So it would just return immediately, like in your lab four, just probably return negative one, be like that thread doesn't exist yet. What are you doing? And then it can just start executing now. And then the amount of different outputs you could get, so you could still possibly get this output, but you're not guaranteed to. So now in the case that join just returns an error, it just keeps on going. I could print thread zero first and then thread zero can be done. So let's say one of my outputs, I print thread zero, then main creates thread one, then I could join on thread zero, then print main, and then I can print thread two, and like there's so many different output possibilities here that, yeah, oops. So for this, I like made this worth five of the six marks. And then if you said any other possibility, I gave the other mark as a way to like, oops, sorry, didn't mean to make it difficult for you. Then it says, assess if there's a race condition, the provided code, explain your reasoning, highlighted any lines of code where shared resource might be accessed concurrently without proper synchronization. So if we rewind a bit, so in the intentional way, was there a data race? No, right? They were all updating the same global variable, yes, but only one thread could have updated it at a single time. Therefore we don't have a data race because a data race is, we have to have two possible concurrent actions to the same memory location with at least one being a right. So it doesn't happen in this case at least, so we're all good. In the other case, definitely happened, so you could just say yes, and just say what the data race was because join has an error. All right, are we all good for that question? All right, other fun questions. So the other one was synchronization. So, oh, I heard it grown already, yikes. All right, so there will definitely be synchronization on the final, so. So it says examine the provided code where there's two threads. They're set up to execute T1 and T2 respectively. So we have two threads set up to execute these. That says each acquire and release multiple mutexes. Say we have three mutexes in total, M1, M2, M3. First question says analyze if two threads executing functions thread one and thread two in the provided code can result in a deadlock. Explain your reasoning as to whether or not a deadlock is possible or not in this scenario. Yeah, so with this, two different ways you could explain it. First, that the only lock that's shared between them is M1 and neither thread is trying to acquire another lock the other one has. So there is no possibility of hold and wait because, well, I'm not trying to get a lock that another thread has. And also, there is not any circular wait here either, right? So T1, while it has M2, it just wants to get M1. If T2 tries to get M1, it wants to get M3. It's not a circular wait, I can't draw a circle between all the dependencies. So no possibility of a deadlock here. So could say either just, if you want to do this really short, no circular wait. Boom, six marks, how easy was that, eh? So that says, assume addition of a third thread, thread three executing function T3, divides a sequence of lock calls involving mutexes M2 and M3, that could lead to a deadlock. Describe the sequence and how it results in a deadlock when combined with T1 and T2. So we want to create another function, oops, void T3 that somehow uses M2 and M3, such that we do have a possibility of a deadlock. Lock M3, then lock, yeah, and then I could, so I don't really need, you could unlock and be good, but we're just doing a deadlock, so we can just leave it at this. So if I have those sequence of calls, I have now created a circular dependency, right? Because now I have M3 that needs M2, and now if I add that to the other two, well there's my circular dependency. So I could run it, thread one gets M2, and then we switch to thread two, it gets M1, and then we context switch to thread three, it gets M, so, and then yeah, thread three gets M3, and then we're screwed. No thread can make progress anymore, therefore we are now deadlocked. So I tried to be creative here and flip the deadlock question to like, you make a deadlock instead of, is this a deadlock? So wow, see, I'm pretty much out of creativity though. So, we'll see what you get. All right, next one. Wow, we are cranking through this. So that was like most of the questions, but long questions, so we're almost done this exam in 15 minutes. So everyone should be able to go home early, right? Yeah, probably not. Huh? Yes. Yeah, so typically to time an exam, I try it myself and take how long I took, multiply it by four or five, and then if it's under the deadlock, then if it's under the exam time, that's about right. So yeah, I think I had to raise it. I think I started at like four and now I'm up to five. So that's the problem, right? I get better as it goes on and you guys are a new class every time, so. Yeah, so I'll probably multiply it by five this time. All right, so next question. Given a struct and the functions for a center four, implementation, so we're trying to implement a center four here, using condition variables. In the provided code, you'll identify and explain any issues. Assume the existence of center four and net value that initializes the center four's mutex, condition variable n variable or n value, focus particularly on thread safety and correct center four behavior. So first question says, assume that the initial center four starts with the value of zero, describe a sequence of context switches between two threads concurrently calling post that could lead to unexpected output, specifically consider the final value of the center four after these operations. So I have two threads trying to call post and for when these questions are set up like that, I had some students to say, assume a call also happens to wait and then they like randomly jumped in the middle of it or something and didn't start from the beginning and I can't give marks for things that are impossible. So please know that function start at the top and go down and if the question says two concurrent calls with two threads to post, you should explain it in terms of two threads calling post. So we have thread one set up here, also thread two set up at the same place. So initial value is zero and then if I have two concurrent calls to post, intuitively what should the final value of the center four be if it worked correctly? Two, right? Starts at zero, two calls to post, it should be two. All right, so it says, is there a way to concurrent threads call post that leads to some unexpected output, AKA any output that is not two and then it even explicitly said, consider the final value of the center four. So is there any way the final value could be zero or one? Yeah, yeah. So plus plus, not atomic. What may happen is that three thread one reads value zero. Can even do it in red, context switch. Then now we're running thread two. It now reads a value of zero and now doesn't matter what happens, right? They're both going to update it in the registers which are independent and then write the value of one to that location so the final value is going to be one. Then, oops, then, wow, my writing's bad, doesn't matter, both, there. That's all you had to write so we have a switch. As soon as they both read zero, then we're screwed, they're both going to write the value one, then we get the final values one should be two. So everyone good with that? All right, I can already guess the next question I'm going to ask, you probably should be able to two. Fix it, yeah. Yeah, so, yeah, so, well, if I was to fix this, right, I need a lock before I update it. Do I need the unlock after the signal? So unlock, so I could put the unlock after the update or after the signal, doesn't matter, both are going to work fine. No, signal just wakes up one thread that's waiting for the condition to change. Yeah, so remember what the condition variables do? They're basically a way to manage a queue. Signal wakes up, if there's threads asleep, signal wakes up one thread in the queue and then the wait function, right, that will atomically unlock the mutex and put itself to sleep and then wait for someone to wake it up and which case when it's woken up, it will try and reacquire the mutex again. So you have to have the mutex locked before you call the function and whenever it returns, it will also have the mutex locked. So I can do the unlock whenever I want, doesn't really matter. All right, oh, that wasn't actually the next question, though, so the next question was, assume we have two threads, one calling post, one calling wait as the code is written. I was nice and even told you what it would be. So I said, is there the case of a lost wakeup with this? If so, explain how a lost wakeup could occur. So I'll give you a minute to think about that. Yeah, lost wakeup thread goes to sleep, never wakes up again. If you want the real-life analogy, it's the thread comatose or something like that. So wakes up, or sorry, goes to sleep, never wakes up, but it's not dead. Yeah, no, we have a lost, we should have a lost wakeup right now. So the modification should fix the lost wakeup. All right, any, any brave soul? Yeah, so T2 locks and then enters a while loop so it reads a zero, right? Reads zero. So yeah, it would read a one, right? Or sorry, read a zero, write a one, and then it signals. Yeah, so signals, nothing happens. Now it's done, the context switch back, then T2 calls wait, then it's asleep. Not gonna get woken up again unless, something else calls post, but right now the value is one. It should be woken up because, hey, the value's one now. You can decrement it, it's fine, but it doesn't because we have a lost wakeup. So everyone good with that? Yeah, how the wake call is related to, so this is just like the name of the condition variable. So the way I like thinking about that is just like that's the queue. So that's the queue of threads that are waiting. And then the mutex is the mutex that protects the queue and that it reacquires whenever it wakes up. So you know that it doesn't have any data races or anything like that. So we have the mutex locked, whatever we call wait, in order for another thread to make progress, it has to add itself to the queue, then unlock the mutex while it's asleep so that another thread can make progress. And then whenever someone signals it to wake it up, the first thing it's gonna do is try and lock that mutex again so that we don't have any data races or anything like that. So because it's, next thing it's gonna do is read that value again, right? So we wanna make sure we don't have any data races or anything like that. All right, so yeah, next is the fix. So the fix, we already said that and then I believe I may have said, explain why these things are now impossible. So lost wakeup is now impossible because well, if we start with thread two, right, we start with thread two, if we context switch, this cannot happen, so this cannot happen because the first thing it's going to try and do is lock the mutex, right? So this can no longer happen anymore. So even if we context switch, nothing is allowed to happen. So it can't make any progress. So we have to switch back to thread two to make progress. Then it would, after it reads to zero, then it waits. So after wait, so I can draw like the queue up here. So initially it's empty, when it calls wait, then what it will atomically do is add itself to the queue, what was that? We called it thread two, and then unlock the mutex. So I could just say mutex is now unlocked, right? So now thread two is asleep, the mutex is unlocked. The only thing we can do is we can context switch back back to thread one, well, then it could go ahead, whoops. Thread one could lock, then it would update the value from a zero to a one. So I'll just shortcut it and just say writes a one. And then right now still nothing else can run. Only thread one is allowed to run. Then thread one unlocks. Soon as thread one unlocks, then it signals. So now it no longer has the mutex. Oh, whoops, I should have written here. So thread one did have the mutex. Now it doesn't. Now it unlocks and then it signals. So now thread two can go ahead and execute. Before returning from that wait call, it needs to reacquire the lock. So I could context switch over to thread, context switch back over to thread two. It reacquires the lock because no one has it. And now it would go ahead and that's why we had the while. So it would go read that value again. Now the value does not equal zero. So it would go ahead and pass and then decrement the value from a one to a zero and we're all good. Now it unlocks the mutex and then we're done. So, and just to be clear, so what might happen if we put the unlock after? So this is what you suggested, right? So let's see what happens there. So let's rewind. So, oops, all right. So let us rewind a little bit. So right now thread two is in the queue. It has hit this weight. So it has hit this weight. So no one has the mutex anymore. It is now asleep. And we're context switching back over to thread one. Right, something like that. So now what might happen is okay. Well, thread one locks rates of one and then signals, right? So if it signals, now thread two is out of the queue, could run and now thread one has the mutex, right? So what could happen right now is we context switch. Oops, I don't know why this isn't blue. So now we context switch. So if we context switch to thread two right now, can it return from the weight? No, why? Yeah, thread one has the mutex, right? So it's woken up and it's not gonna be able to get the mutex. So technically, if you really, really, really care about like micro-optimizations, you should have the unlock first and then the signal. But it's not gonna affect like the correctness at all. This might be a bit slower because now we can context switch to thread two. Thread two would try and acquire the mutex, it can't. So because thread one has it, so we have to context switch back to thread one. Yeah, so it would try and lock the mutex again and then it would go into like the mutex queue instead of that like a condition variable is basically a queue we can manage, but the mutex would also have its own queue. So there's queues on queues, and which is great because we love linked lists, right? So that's more linked lists for us. Yeah, so now we have to switch back to thread one, then eventually it would unlock and then finally thread two could get the lock, then return from weight and then check. Now the value is one, break out of the loop, go ahead, decrement it, everything's good. All right, any other questions about that one? I think that covered everything and we got a little bonus. So that's good, that's good. Oh, okay, here's a fun question. We should have lots of, more than enough time for it. So this now says, assume your center four works. Assuming a functional center four analyzes a given code where it initializes the center four. So right here we initialize the value to be zero and then we create two threads. So thread one executes one thread, here I'll call it something nicer. So thread one wants to execute this function and then we have thread two that wants to execute this function. So our goal is we always want to print tic-tac-to and always want to do it in that order. We're not allowed to move the print-off statements between the threads or anything like that. So first question is analyze if this implementation can lead to a deadlock and that's like a deadlock in any thread, like can any thread be stuck where it can't make any progress? So it can. You're close in weight and then the other one arrives and it weighs. Yeah, so what may happen is thread one, so here's our center four value. I'll just write it over here. So currently the value is zero, right? What could happen is thread one executes, we post. So now the value is one and it would do that atomically. Then, well thread one can continue executing and call weight, then weight would update that value from a one to a zero, then it would print toe. So we would see tic-toe and now if we switch back to thread two, it's at a weight, the value is zero, it will never change, right? Now it's stuck, thread two gets deadlocked. So can't make any progress anymore. So there is a case where it would because ideally what we want to happen and if you actually run this it will generally actually print tic-tac-toe most of the time because what we were thinking is, well we print tic, then we post, right? So we change the value from a zero to a one. And now we want to context switch to thread two, then thread two can weight, change the value from a one to a zero, then print tack. And right now if we context switch to thread one, thread one can't make any progress, then we would post whenever, after we done, after we are done printing tack, update that value from a zero to a one, then thread two can go or thread one can go ahead, pass this weight and then toe and then it's in the correct order. But yeah, what might happen is it deadlocks or at least one thread gets stuck. Then next question was modify it so that it is correct. You can use additional center fours if you want. Go ahead, directly annotate the code if you want and or just describe it and refer to line numbers. So yeah, and spoiler alert, this question came up because someone asked, hey, can I just use one center four for something like this? And then also we had a foster dog that was named, what was her name? Tack, because they were three of them and they were tic-tac-toe, so I merged both of the ideas and boom. If you ever want to know how I come up with questions, it's purely random. All right, so give you a minute to think about how we would fix this. Yeah, or we'll wait. All right, any solutions? Yeah, yeah, so I need two center fours so I'll create another one, some of two, so they both wait on different center four. So this should be, I'll put, I'll make that better. So this should be some two, okay? So this should be, and then what should the initial value of the center four be? Zero, boom. So there's our fix. So that should work now. So now if thread two is waiting on, whoops, if thread two is waiting on the first center four, it can't make any progress, right? The value is currently zero, so it's stuck, can't make any progress. Thread one has to run, print tack, and then it would post SEM like the first center four, so it would increment it and now it can't make it pass this because its value is zero. So thread two is the only thing that could run, it could make it by the weight, print tack, and then, well, if we switch back to thread one, still can't make any progress because the value is still zero. Now thread two can go ahead, post the other center four, changes its value from a zero to a one, then thread one can actually pass this line, and then we get tow, so we get tic-tac-toe, cool. All right, all good with that question? All right, seven minutes. Should we try and speed run a question? All right, let's try. So other one that is hard is probably this process one. So this process one was, I'll just try and describe what it actually does and then hopefully you can answer the questions. So this one was a little bit more involved because it actually tested some file system stuff too. So what this does is there is a global variable called initfds because we're going to call pipe on it and then share it between all the processes. So we have process 100 here, it calls pipe, so we make a pipe with fds, we assume it's successful all the time so that we have a read-in and a write-in and then we create two processes. So here, let's assume that child one and in the create process. What it does, I was nice, we fork, then we don't know which one is going to run. Assume we have processes 100, 101, and 102, so if PID is greater than zero, that means we are the parent, I just returned the PID, so we get the PID returned and then all the child does is call read-int, which all read-int does is read an integer from the read-end of the pipe and that's all it does and then it exits. So the child children don't really do anything that involved, so it would just look like this. So we get 101, probably 102, as the two return values in our parent process, then our parent just writes. So right now we don't know what order things would run. If right now the child ran, so let's say they were both here, right? They call read-int, which reads from the read-end of the pipe. Currently there's no data in the pipe, right? So read is a blocking system call, they'll just sit there and wait forever until we actually write to them, right? We don't know which one's going to execute, we don't know anything about that. Then eventually the parent process is going to call write-int and what write-int does is just write an integer to the right end of the pipe. So now it writes the number 42 to the right end of the pipe, so here is our fun pipe, so it puts like a 42 in there. Now, we don't know who's going to run next. Maybe it writes 1337 next and there's two numbers in the pipe, don't know. Maybe process 101 executes next and reads an integer in the pipe, don't know. Maybe it's 102, don't know. So anything could possibly happen. What could happen is let's say 102 could read. Now it would read 42 and because there's like a position in the buffer and since everything's shared in that global open file table, after it reads that, it would advance the position to here. Whoops, move. Okay, moving too much. Advance the position to here and now, well, process 100 could go ahead and write 137 to the pipe. Now, if process 101 wakes up, it could go ahead and read 1337 and now they're done and there's no way that I can possibly read 42 twice because well, everything got forked together. It's the same global open entry. They share the same position. They share the same everything. So we won't read the same number twice. So that is one possibility we might have and then process 100 closes the right end of the pipe so that we can actually know that no data is possible. So after they read, then they'll exit and then close all their file descriptors and then process 100 would be the only one with a right end of the pipe open. If it closes it and then waits for the child, then whenever it calls end of output, then all of the right ends are closed. So there's no possibility for some output. So read will just return zero and then we would get end of output returns true and then we're done, right? And other possibilities are, well, process 101 could have read 42 first, process 102 could have read 1337 after and other possible outputs is, well, maybe process 102 read 42, but it didn't actually got context switch before printed. So another possibility is just like essentially this with what they read, but in the other order, so we could get 101 printing off 1337 first, this printing off 42, or similarly I could do the other order of this. So 102 could read and then 101 reads 42 and this is where they get context switched like fairly subtly, so they get context switch right after they read and before they print. So I made that worth last, yeah. Yeah, but I also wait for the children here to finish. Yeah, and one of the questions you can read on is like, what happens when I remove this? Then I think the other one's like, what happened if I remove this and then like, does this always print? And you can go through that, but hey, we're out of time now. So thanks for sticking with me the whole time. And yeah, we're done. So just remember pulling for you. We're all in this together and I owe you two hours.