 I did do. OK, now it's recording. OK. Hi, everyone. Welcome to the first recitation of CSE 421-521. My name is Ali, and Carl is over there. We're going to be your TAs who will teach recitation. We will be rotating per week. So each one of us will handle one week. The next week, for example, we're going to be Carl. We will have several recitation per assignment. For example, we're going to have two recitation for assignment one, three for assignment two, and then we're going to have four recitation for assignment three, in addition to recitation for the midterm and final. What should you expect from our recitation? Basically, it will help you get started. We will show you what are the files that are important. As you might know, the source code that you've already cloned has a lot of files, and pretty just like confusing if you really want to go through these files and figure out what's not important for your assignment. So we will point these files out for you. And we will also show you where should you go and start implementing code in these files. We will also just provide you some hints on the assignments. So we will help you get started, show you the important files, go through the, for example, the implementation that is already provided to you through the source code. And then we will provide some hints for you so that would help you to implement your assignment. Also, we're going to be recording the recitations. Every Wednesday, I'm going to post the slides and the videos on the website, or just going to post them. And I will post them also on this course. So you will have them by Wednesday. So that's for the logistic. Any question about the recitation? OK, so let's get started with assignment one. So for today, I'm going to review, go through the assignment one. What should you implement? I'll present you to the critical section. What is the critical section? Then we're going to present you to the locks and then spin lock, weight channel, semaphores, and condition variables. You'll get an idea of what each of these are because in order, for example, for you to understand what is a lock, you need to know that there are some part of your code that is a critical section. We name it a critical section because, for example, it has access to a shared resource. So these are the building blocks for each other. So we're going to go through them one by one. Hopefully, if the time allow, we will also cover the condition variables. If not, we're going to cover it next week. So assignment one, you're going to implement synchronization primitive. That will allow your code to synchronize. So you're going to implement locks. You're going to implement condition variables. And then you're going to implement reader writer locks. After that, you need to solve synchronization problems like the well-meeting problem and buffalo intersection problem. For these problems, after you implement these synchronization primitives, you need to figure out which one are the most appropriate for each problem. Once you're done, this is assignment one. And then you have the deadline, which is going to be on Friday, February 17, which is around one and a half week from now. So let's start with the critical section. Critical section basically is a piece of code that, let's say you have a program that has multiple processes. And there are a piece of code in your program that these processes need to access at the same time. This part of your code, we call it critical section. And this part of your code, because basically it will have access either to data structure, to shared resource, you need to protect that. You cannot allow more than one process to access that shared resource at the same time. So critical section, as we said, doesn't allow multiple concurrent processes to access that shared resource at the same time. Let's say you have a printer that's shared between five PCs. If all these five PCs access the printer or send the print job to the printer at the same time, what will happen? Either things will get mixed up while printing, or the printer basically will crash. So let's try to say that in your program, you want to define a function that will print. So your first version of the function print will be basically it's a void safe print. It will get the job and do print. The command will send the job to the printer to print. So now this will not work. Why? Because as I answered this before, this is a shared resource. You have five PCs. There might be times that multiple PCs will send a print job at the same time to the printer. And this will basically not work. You will get mixed up. Either things going to be mixed up at the printer, or as I said, the printer will crash. So now we need a lock. This is where a lock comes into play where we need to protect that shared resource. And the lock basically is a synchronization mechanism that will make sure that a shared resource does not get accessed by multiple processes or thread of execution. Wherever you have multiple threads of execution and you have a shared resource, you need to protect that shared resource so it does not get accessed at the same time by more than one thread. So basically it's a way for you to just like enforce a control policy on the shared resource. So what are the interfaces that you need for a lock? Basically, you need to create a lock. So let's say for the printer, you need to define a lock that is specific to that printer. So you can, for example, name it printer lock. So this is one of the interfaces that you need. You also need lock that's destroyed. We want to make sure that whatever you allocate is a freed and you're not leaking memory. This is very important, especially for OS161. We are checking for memory leak. So you want to make sure whatever you malloc or came malloc, you're going to free it later in your code. If you do not free it, you're leaking memory. And you have the tools in the kernel to check if you're leaking memory or not from the kernel menu. I think you can find this easily. Then the third interface that we would need for a lock is lock acquire. That means any process that wants to access that shared resource needs to acquire the lock first and then access the resource. And the fourth interface would be basically lock release, which is releasing the lock after you're done from the shared resource. So now this is our example should look like. You have the safety print function. First, whatever thread is calling that function, it should first acquire the lock, send the print job to the printer, and then release the lock, giving the fact also that the lock is available. If it's not available, we will talk about that scenario later. So now where can you find locks in OS161? So locks are basically you have the function declaration defined in this file, sync.h, and you should be implementing these function in this file, which is sync.c. Let's go through the file. So this is sync.h. As you can see here, if we go down, here's the part where the lock structure and functions are defined. So we have a lock structure that you need to add variables to it as you implement your locks. Then you have lock create, and you have lock destroy. You also have some comments that will help you understand what each function does. So you also have here lock acquire, lock release, and lock do I hold? That's basically do I hold will tell the thread if it's holding the lock or not. Why do we need it? Let's say recursive lock. I acquire the lock and then in function A, and then I go into function B, where I need also to acquire the lock again. So instead of reacquiring the lock, I can just query whether I'm holding the lock or not through that function. So that function will basically return Boolean as you can see that says if you're holding the lock or not. So I know this seems vague to you now, but it's going to get more clear as we move on, and especially if we go through the semaphore implementation, which is already implemented and given to you. So this is sync.h. And if you go to sync.c, you could see that the functions are there, but they are empty. Nothing really in there. So in create, you have some of the, for example, you have the name, which is already given to you in the structure. And here we are allocating the name. So when you create a lock, you need to give it a name. But that's not really important. You shouldn't identify the lock by its name. So the name is passed to you as you create a lock, and you need to create or allocate the name variable. So this is the create, and that's destroy, which will basically destroy or free your lock that you allocated. And you need to make sure that whatever you create or whatever you allocate within the create, you're going to free when you destroy. So this is one of the checkpoints that you should always have whenever you're defining the create and destroy functions for a variety of stuff that you're going to implement later on in this course. And then you have lock acquire and lock release. And then lock do-i-hold. So you have the function definition, declaration, and you have the actual function given to you here, but you need to add stuff to it that's empty. Later on, let's say starting from assignment two, you won't get this stuff. You need to just declare the functions, whatever you need, and implement the actual function. But for assignment one, you're given all of this here. You're given this for locks and condition variables, and also the reader writer locks. So let's go back to the slides. Now, what are the... So in order for you to build a lock, there are building blocks for building a lock. So we already have a lock that is implemented for us in OS 161 called the spin lock. And you might wonder, since we already have a lock, why shouldn't we use that spin lock instead of the lock that we're going to implement? So the only difference between the spin lock, basically, just like, let's say you want to acquire the spin lock. If you try to acquire it, and it is acquired by another thread, you will keep spinning on the lock. Are you available? Are you available? Are you available? So you're going to be on the CPU without just releasing the CPU and keep bugging the... Just like the spin lock, ask him, are you available or not? So what we need to do here is to implement a sleep lock. So the lock that you're going to implement, basically we call it, other than its named locks, it's basically a sleep lock, which will allow a thread, if it tries to acquire the lock, and the lock is not available, it will go to sleep on a weight channel. So that's the difference between a spin lock that you already have implemented in OS161 and the lock that you're going to implement now, an assignment one. So spin lock will keep spinning while sleep lock, if it's not available, you will go to sleep. But to implement the sleep lock, you do need a spin lock. Why? Because still the lock, so still it's a shared resource. And you're going to see this through, if I go through the semaphores, I'm going to show you, you have some shared resources within this implementation that you still need to protect. And you shouldn't allow more than one thread to try to access the sleep lock. So spin lock is one of the blocks that we're going to use to build the sleep lock. And it is defined the function declaration for a spin lock is defined in spinlock.h, and you can find the actual function in spinlock.c. Now, you don't really need to really know everything or how the spin lock is working. What you really need to know is what are the functionality, what are the functions that are given to you by the spin lock? So if we go to spinlock.h, we can see that here are the functions that we have for a spin lock. Basically, a spin lock, and it will initialize the spin lock, clean up, similar to destroy. You have spin lock acquire, spin lock release, and spin lock do-i-hold. So you're going to use these to implement your sleep lock. So as I said, one of the blocks are the spin lock that's provided to you that you're going to use to build the sleep lock. What are the other two that you do need to implement the sleep lock? You have the lock, but you don't have the weight channel. So you need a weight channel so that a thread can go to sleep whenever the lock is not available. So weight channel is also defined for you, is already given to you, and you don't need to worry about that. So you have all the building blocks, you have the spin lock that is already implemented for you. You have the weight channel. You only need to put these together to implement the lock. So let's go through the spin lock function declaration and the code itself. You also have a thread.h. You can go through that, which is also useful, but you really don't need to understand everything. It's good to go through thread.h, which will provide you with the function definition for the thread, just like fork a thread, stuff like that. But you don't need really to go into how these functions are implemented for now. So let's have a look at the weight channel and see what are the functions that are provided for us. So basically whatever, let's say for the synchronization primitives, whatever I think that you need to define in OS 161, you do need the create function and destroy function. So as we can see here, we have the wchan create, which will create a weight channel for us. And also the destroy function, which will free the memory that we allocated in the create. We also have the wchan is empty. This you can use to check if the weight channel is empty or not. Also, we have that useful function, which you're going to use, which is the wchan sleep. This function that you're going to use to put the thread to sleep, if the lock is not available. And what does it take? It takes the weight channel for that shared resource that you defined. And then it will also take the spin lock, or the weight channel for the sleep lock that you already defined. So when you define a lock, you're going to have a spin lock, you're going to have a weight channel. And that's what you're going to pass when you call wchan sleep. You're going to pass the weight channel for that lock. And then you're going to pass the spin lock for that lock to the weight channel. What does a weight channel basically will do when you go to sleep? It will basically release the spin lock, put the thread to sleep. Whenever the lock is available, now we're going to wake one of the threads. And now when you wake a thread, you will acquire the spin lock again and continue. Or you can wake all the threads that are sleeping on the weight channel using the weight channel, wake all. So you have weight channel, wake one, which will wake only one thread. And weight channel, wake all, which will wake all threads that are sleeping on the current weight channel. So this is how we should go about implementing a lock. Now you have just like a high level understand of what is a sleep lock and what are the building blocks of this sleep lock, which is spin lock and weight channel. Now how should we put things together? So let's say now we have that function. And this is what I'm going to present to you through the Simo, for example. So let's say we have five printers and 10 PCs. Will this function work? Anybody? We have five printers that are shared resources. We have 100 or 10 PCs. Do you think this will work or not? Nobody? So this will basically partially work. Why? Because the lock will give us access to only one shared resource. It will give us access to one printer and not five printers at the same time. What would be a better solution to this? It's a Simo4. Simo4 basically is a synchronization primitive that will control access to a set of shared resources. Not only one, you have a set of them. And you should use a Simo4 with them, not a lock. Because if you use a lock, you can only acquire one lock at a time. So if you have five printers and five PCs are trying to access these five printers at the same time, they won't be able to. Only one of them will get access through the lock. So what should you do? You should use Simo4. So in order to do this, Simo4 has one extra variable within it defined, which is Simo4 count, which that you will set when you define a Simo4. You will set it initially, just like once you create a Simo4 for these five printers, you need also to pass the number of printers that you have, which is five. This will set the count. And then we will use the interfaces, which is called P and V. P will basically decrease the Simo4 count. And V will increase the Simo4 count. So let's say all five printers are printing at the same time. What will happen now? If another thread, while these five threads are printing, want to access one of the printers, we will put that thread to sleep. Because the Simo4 count is equal to zero. Once one of them is done with the printer, they will call the function V, which will increase the Simo4 count. And at that point, whatever weighting thread on the weighting channel will be weighted up and will get to print using that printer, which is released now. So let's go through the implementation. So this is how our function will look like. And instead of lock-acquire, so P is equivalent to lock-acquire. V is equivalent to lock-release. So you have five printers. As long as the Simo4 count is more than zero, threads can get in. Once the Simo4 count is less than or equal, let's say equal to zero, because it won't get less than this. Once it's equal to zero, we will put this thread sleep to sleep until one of the threads releases one of the printers, which will increase the Simo4 count and will let that sleeping thread to wake up and go into the critical section. And that's why some would call a lock binary Simo4, because locks are basically a special case of Simo4. So with Simo4, you have a set of shared resources with a lock you only have one shared resource. But there is a significant difference that doesn't mean you should go through the implementation of Simo4 and exactly do the same with a lock. That will not work. The best thing you can do is you go through the Simo4 implementation, you try to understand it. And then based on that, you try to implement the lock. Why? Because with the locks, the only difference that we have between a Simo4 and a lock that a lock can identify itself. We have the function, do I hold? While you don't have that function with Simo4, they don't care about who's holding the lock. But with locks, you have the function that should tell you which thread is holding the lock. So there are going to be some differences within the implementation between a lock and a Simo4. So where can we find the implementation for Simo4s? You can find the declaration for the functions and sync.h. And it is implemented for you and sync.c. Let's go through that. So this is the Simo4 function declarations and structure. And it's fully implemented for you. So the first step you should do, go through the implementation here. This will answer a lot of your questions. So as you can see, the structure Simo4, what does it has? It has a name. It has a weight channel. It has a spin lock. And it has a count. And that's basically what you need to build a Simo4. And then you have the Simo4 create, which will take the name and the initial count. When you create a Simo4 for a set of shared resources, you need to tell the Simo4 how many shared resources you have to control it. So here is where you pass the initial count for the Simo4. Then you have the sum destroy, which will basically pass a pointer to the Simo4 structure. And you start freeing whatever you've allocated. Then we have P, which will increase. This is equivalent to lock acquire. V, which will decrease the Simo4 count equivalent to lock release. Let's go through the implementation. So now this is the function Simo4 create. As we said, it takes the name, string, and it also takes the initial count. Now here, you're going to create the Simo4 structure. And then you start to allocate memory for every variable you define in that structure. So you need to allocate memory for the structure itself and then name. And then you need to allocate memory for the weight channel. And then you need to initialize the spin lock and set the Simo4 count to the initial count that you've received. And then you return the Simo4 that you've created. You will do this only once. Once you're done with the Simo4, you make sure it is deleted or destroyed. And for destroy, you take the pointer for the Simo4 structure and you start cleaning up. How do you clean up? So you created a spin lock. You use the functions for a spin lock. We have a spin lock create. We have a spin lock clean up. You created weight channel through weight channel create, as you can see here, you need to free it or destroy it through its own functions. And then the name, which you manually created, so as you can see here, you need to free it because you don't have an interface for it. So make sure to use the interfaces for every tool that you use. You use spin lock, use its interfaces. You want to create or define weight channel. You want to use its own interfaces. So this is SimDestroy. Now let's go for the p function, which is equivalent to the lock acquire. What will happen? Basically, what I'm going to do now is I need to acquire the spin lock here. Why do I need to acquire the spin lock? Because now to the Simo4, the Simo4 itself, it has a shared resource. What is the shared resource for the Simo4? Anybody? So the shared resource for the Simo4 is the Simo4 count, which can be accessed by multiple threat at the same time. And you want to protect that. So here, you're going to use a spin lock. First, you acquire the spin lock and you pass the spin lock that you created to the spin lock acquire function. Then now you're going to loop here. While the Simo4 Sim count equals 0, I'm going to put the threat to sleep. So if there are no more Simo4s available and the count is equal 0, instead of spinning, you put the threat to sleep using the weight channel sleep. And you pass the weight channel for the Simo4. And also, you pass the spin lock for the Simo4 that you defined. Why you do this? Because the weight channel sleep will basically release that lock and put the threat to sleep. Let's say there are Simo4 available, then you won't get into that loop or the while loop. You will basically decrease the count of the Simo4 and release the spin lock. So as you can see, the shared resource for the Simo4 is the count and that's what you really need to protect. Once you decrease the count, you release the spin lock and at this point, you have acquired one of the Simo4. You can go ahead and print. So this is going to be here. It do print job. So once I'm done with, once I release the spin lock and the P function, I'm going to go back here and go ahead and send the job to the printer. And then we have the V function, which is equivalent to the release lock function. What I'm going to do in the V function, again, because you want to access the Simo4 count, you need to protect that through the spin lock of the Simo4. So first, you're going to acquire the spin lock and then you're going to increase the Simo4 count. And now, since you released one of the shared resources, you need to wake one of the sleeping threats. So you're going to use weight channel wake1 function to wake one of these sleeping threats. You're going to pass the weight channel for the Simo4 and the spin lock for the Simo4. So once you increase the count and you wake one of the threat, you can now release the lock. So this should give you a good example of how should you go and implement the lock. Any questions? So anybody started implementing the locks or not? OK. So now, as we can see here, you have now the lock create, lock destroy, lock acquire, release, and then do I hold? Look through the implementation of the Simo4 and then try to implement these functions one by one. So this is the sync edge and sync.c and sync edge. So here, you can see that we, as I showed you, you have the lock structure, which already has the name in it. Now you need to add stuff here. You need to see what do you need to implement the lock. And then you have the function declarations already given to you. You don't need to do anything. And you can see what's the signature. So for example, the lock you create only receive a name. It doesn't need to receive an initial count like the Simo4 because the lock will only protect one shared resource. The destroy will, again, receive the lock structure, acquire, release, and do I hold? You will basically pass the lock structure. Through the structure, you should be able to identify. You should be able to see if the lock is available or not. So to acquire it or release it or check if you hold the lock or not. So that's basically how you should go about implementing the locks. Now if you don't have a question, I can go ahead start with the condition variables. So any question on locks, Simo4s, weight channels, spin lock? OK. So let's move forward. Now after you implement the lock, you need to implement the condition variables. What is the condition variable? Condition variable basically will let a thread to sleep until a condition become true. So you can say that it is similar to a weight channel. But a weight channel receives a spin lock while condition variables receive sleep lock, the locks that you're going to implement. So you will have some cases where you need to wait for some condition to become true to proceed. And this is where you need to use condition variables. So how does it work? Let's say you acquire the lock, and then you see that that condition doesn't hold. It's not through yet. So instead of keep holding the lock, you will release the lock, go to sleep on the weight channel. And then once the condition becomes true, you will wake up and acquire the lock again. So this is basically how the condition variables work. And it is, as I said, similar to a weight channel. But keep in mind, weight channel takes a spin lock, but your condition variables take a sleep lock that you've implemented. So where can we find condition variables in OS 161? You can find them in these files, and you have all these interfaces defined for you for condition variables. Let's go through them one by one. So this is the part for condition variable. You have the structure. Again, you're going to have a structure. So things keep repeating, as you can see. You have the structure. You have condition variable create. You have condition variable destroy. And then you have the functions or the functions that will define the condition variable functionality like CV weight, where you're going to put a thread to sleep, CV signal, where you're going to wake one of the threads that are sleeping on the weight channel, and CV broadcast. So if you want to compare this to the weight channel, as we remember, for the weight channel, we had three functions, weight channel sleep, weight channel wake one, weight channel wake all. So weight channel sleep is equivalent to weight channel weight. But here, as you can see, it takes the condition variable and the sleep lock that you've implemented. CV signal is equivalent to wake one, weight channel wake one. And CV broadcast is equivalent to weight channel wake all. So CV weight will basically release the lock. Three things you need to keep in mind. It's important. Release the supplied lock. Go to sleep. Once the condition is true, you will wake up and reacquire the lock. This should happen within CV weight. CV signal will basically wake one thread that's being sleep on the provided or supplied condition variable. And CV broadcast will wake all the threads that are sleeping on the condition variable. So CV weight, I'm going to repeat this. You're going to release the lock. Go to sleep. And then once you are waked up, you need to acquire the lock within the CV weight. So these are the interfaces for condition variables. Let's go through an example. A very known example for condition variables are the producer and consumer example. So we have a producer that produces items until the buffer is full. And we have a consumer that consumes items until the buffer is empty. So how should we coordinate between these two? We're going to use condition variables. This is how the code should look like. So for the producer, we'll basically produce an item first, and then I'm going to acquire the buffer lock. So the buffer is the shared resource here. We are going to acquire the buffer lock. Then you're going to check if the buffer size is max, which means the buffer is full. If the buffer is full, you should sleep. You should put the producer to sleep until the buffer has space for one more item to be produced. So once the buffer is not full, you're going to wake up the producer. So while the buffer size equal max, you're going to put the producer to sleep, and you're going to supply the buffer lock. And here, the buffer lock now, which you acquired, will be released. You will put the producer to sleep. And then once we signal the producer through the consumer, the producer will wake up, acquire the lock, and then continue. So when will we be continuing or going after the while loop? Either the buffer is not full or the buffer is full and you're sleeping, and then one of the consumers will wake you up. Once you wake up, you will continue with this line of code, which is buffer add item. So you're going to add the item which you already produced. You're going to add it to the buffer. And then once you add an item to the buffer, now here you're going to signal one of the consumers telling the consumer who is sleeping waiting for an item to be available in the buffer that now there is an item available for you in the buffer. You can go ahead and get it in consumer. Then you're going to release the lock buffer. So if we go for the consumer, it's going to be the other way around. You're going to acquire the buffer lock. And here, let's say the buffer lock is acquired by the producer. At this, what will happen? We will put the consumer to sleep until the buffer lock is available. Then the consumer would acquire the buffer lock. So if the buffer lock is available, the consumer will acquire it. And then it will check if the buffer has an item in it or not. So if the buffer is empty, I'm going to put the consumer, as you can see here with the producer. I did put the producer to sleep. While here, I'm putting the consumer to sleep because the buffer is empty and to provide the lock. Again, release the lock, put it to sleep. Once the producer produce an item, it will signal the consumer condition variables, which will wake the thread that I did put to sleep. And the thread now here, the consumer can continue. So it's either I'm being waked up as a consumer, I can continue, or the buffer has something in it. At this point, I can just skip the while loop and then continue to the next line, which is item equal buffer get. I'm going to get the item from the buffer and then consume the item. Once I consume an item, I'm going to do the other way around. I'm going to signal a producer that I did consume one item. Please produce another item. So that's why we're going to CV signal and we're going to pass the producer CV and the buffer lock. This will signal one of the producers to produce an item. And then I'm going to release the lock. So this is where condition variables become useful. Any questions? Yes, sorry? So yeah, the condition variable are the CV. So you should already have CV for consumer and condition variable for a producer, as you should have a buffer lock. So you pass that CV. So a producer should have its own condition variables, its own channel, weight channel, and consumer should have its own condition variable and channel. And you're going to exchange those, depending on which one you're working on. Any other questions? So that's what we have for today. Thanks for coming. Let me know if you have any questions. Or you can come to our office hours. Thank you.