 So everybody, let's get started So what are we talking about last time? semaphore, log, and one concept, one very important concept in the seminal one. What is that? Critic section, right? So what is a critic section? Anybody? Access the shared resource. It doesn't have to have our shared resource, right? So a critic section is basically a piece of code that first of all access the shared resource and then secondly, that shared resource cannot be accessed at the same time by multiple threads, right? That's why we need to synchronization. And remember last time the example we used, the printer example, right? The printer, one single printer cannot be used by multiple users at the same time. There has to be some synchronization or coordination happened at the printer side, right? So we talked about log and log is the way or primitive to enforce this kind of mutual access, mutual exclusive concurrent access and log has two primary interfaces. One is log acquire, which indicate whichever thread called this log acquire, that thread wants to access the shared resource or enter the critic section. And we have a log release, which indicate the thread is done with a critic section and whoever wants to acquire the log can go up and grab the log, right? So we don't talk about the details of the spin lock last time, right? We just show the interface of the spin locks. But I did cover the details of spin locks on Friday. So I will show it, explain it in a little more details. So if you look at the code for spin lock, remember what's inside a spin lock structure? We have holder information, which is the structure CPU pointer, and we have a variable indicating if this spin lock is available, right? So inside a spin lock acquire, we first disable the interrupt so that no other thread on this CPU will compete the spin lock with me, right? Then we check the status of the spin lock. If it's available, then I set it to unavailable and I grab the spin lock. I'm done with this spin lock acquire, right? So, but if I come in and the lock status is not available or this lock has been acquired by somebody else, then what do I do? There's no way to handle a spin lock. Right? Spin lock is the one which has already been provided to you. You can look at the code of how the system implements spin lock. Spin lock, as the name suggests, it spins, right? So when it finds out the lock is not available, instead of doing smart things like in a web channel and wake up and all that, it has to keep playing the status, right? Keep spinning there until the lock is available. That's what the spin lock does. Right? In the assembly one, you are supposed to implement another kind of lock which support exactly the same kind of interface which is acquired in the release, but with different mechanisms. So in the in the lock acquire use, you implement, you are supposed to instead of keep playing the lock status, you are supposed to pull the thread to sleep. And then on one other thread called lock release, you are supposed to wake that previous thread up. Wake it up, right? That's what you are supposed to do in for lock implementation. And the last time we also talked about a semaphore, which is the way to manage a set of resources. So in the lock, in the case of lock, we only have one resource, right? Only one thread can inside the critic section. Now, in the case of a semaphore, we have a bunch of resources. So we can allow up to a certain number of threads in the critic section, right? So we go over the code of the implementation of semaphore, and we look at how the semaphore use a sem account to keep track of how many resources left, and how the semaphore use a spin lock to protect the access to the sem account, right? Remember in the function P, we first acquire a spin lock, then we check the count, right? If the count is zero, meaning no resource left, we do what? We put ourselves in a wire channel, right? After wake up, I acquire a spin lock again and recheck the sem account. That's what we do in the case of semaphore, right? Also, as we said, the lock implementation is quite similar to semaphore. The procedure is quite similar if you think about it, right? So you have basically a status indicating if the lock is available, right? You should have a lock, which is a spin lock, to protect the access to that status, right? So inside the lock acquire, what do you do? You first acquire this spin lock, which are supposed to protect the shared status, then you check the status, right? If the status is not available, then you are supposed to put yourself in a wire channel. And then very similar to what the semaphore does when you wake up, you check the condition or check the lock status again, and keep doing that. So this is the hint we can get from semaphones to how to implement normal locks or sleep version of the locks. Any questions before we move on to CV and all that? Any questions? Yeah. Yes, basically a wire channel is where a thread can wait. So a lock can have a wire channel. That means whoever on this wire channel are waiting for this lock, they're not waiting for something else, right? Imagine a system where you can have multiple locks. Each lock has its own wire channel. Different threads may wait on different locks, right? That's why each lock has its own wire channels. Any other questions? So everybody got a lock working? Everybody passed the SY2 test? Good. So today we're going to talk about CV. We're going to talk about read the right lock and we will talk about the synchronization problems. So first, CV. The name itself is a little bit strange. So what is the condition variable? We have a condition in the name, right? So basically in short, CV is a primitive that can let a thread temporarily give up the mutual access to a certain resource. Then the threads are supposed to go to sleep. After the thread wake up, it should regain the access to that mutual access. This all may sound a little bit abstract to you. So how many of you have used CV before? You have? Only one? So I guess that's most people have haven't used CV, right? So it's helpful to... So here's the thing about CV. The implementation is very straightforward. The comments in the header files tells you exactly what you need to do inside each CV weight or CV signal, right? If you go to the kernel, include the single edge and you look at the comments around the CV function declaration. You will find the comments in saying what you are supposed to do inside each function. It's very straightforward to implement, but it's not straightforward to understand the usage of the CV. So to help you understand the usage of the CV, let's go over two examples of how you will use CV. One example is kind of interesting that is that how will you use CV to implement semaphore? Remember what semaphore does? Semaphore inside a P, you want to check the semacount, which is a what? A condition, right? You are counting on the condition that the semacount is positive. If it's not, then you should wait for this condition, right? So think about it. How are you going to use CV to implement semaphore? So suppose you are giving CV. You are supposed to implement semaphore. How are you going to do that? I have a few markers, so let's try to do some coding on the, oh, it's chalk. So P, okay? A function called P inside a P, so similar to the existing implementation, we still have a kind of status of the semaphore, which is the count of how many resources left of this semaphore, right? We also, we still have a count. We still have a lock to protect the access to the count. Now, we also have a condition variable associated with this semaphore. So the first thing we do when we enter this P is what? Because you want to check the shared resource. What are you supposed, what do you want to do at the first step? Grab the lock, right? So you lock, you acquire the semaphore lock here, then what do you want to do? You want to check the lock, the semaphore status, right? You want to check how many resources left while this count equals to zero. That means you need to wait, right? The resource is not available. How can you wait? Like I said, like we said, we use CV, right? CV has an interface called CV-wait, which lets you wait for a condition. The first argument should be the CV. The second argument, suppose it is L, is the lock, right? Now remember what do you do inside this CV-wait? The first thing you do is give up the access to this lock. You want to call lock-release. Now you can see why you want to call lock-release inside a CV-wait. Okay, now you go to sleep, right? After you wake up, you call lock-or-quiet. This is basically what you need to do inside a CV-wait, right? So now you have some idea of why you need to call lock-release before you go to sleep. The same reason why we call lock-release, spin lock-release before we call wet channel sleep, right? If you don't call lock-release, now you are still holding the lock, then you go to sleep. In that case, nobody else will be able to change the count or the status of the semaphore, right? You want to temporarily give up the mutual access to this count. So you give up here, you wait, you wait for some condition, and later on in the V function, somebody will wake you up. And when you wake up, you regain access to the lock, right? So this is what you do inside a CV-wait. Now, after a CV-wait, the question is, do you still hold the lock? As far as this function concerns, no. You acquire the lock when you wake up, right? So this function as a whole doesn't change the status of the lock as far as this function concerns, right? Before I call this function, I have the lock. After I call this function, I still have the lock, because this function will acquire the lock for me, right? So this function doesn't change the ownership of the lock as far as the user concerns. Now, after a CV-wait, I know that this count might be positive. The condition might be true. So I go back and check the count again, right? So instead of the chunk of code you see in semaphore, I can just use one CV-wait to achieve the same effect as what you have in existing semaphore implementation, right? And after that, after this while loop, I know that the count is positive. So I call count minus minus. I decrement the count by one, and I release the lock and I return. So this is the implementation of semaphore if you are given CV, right? This is one example usage of the condition variable. Here, the condition being this count is positive. I'm waiting for this condition. As long as the condition is not true, I wait until this condition becomes true, right? Now, in the V function, you can imagine it's quite simple. You acquire the lock. You call CV signal to wake up somebody. You increment the count. You release the lock. That's it. The V is always simple, right? So now everybody understands how you can use CV to implement the semaphore. Any questions? Well, you call this function because you want to acquire a resource, the P function, right? If you don't want to acquire a resource, then you don't call this function at all. Yeah. Any other questions? This is one example of how you can use CV to implement the semaphore. The second example is kind of a classic synchronization problem which is called producer and consumer problem. Suppose you have a fixed size buffer with a size, say, n. Then you have a kind of thread which is called producer, which just keeps producing items and puts this item to the buffer until the buffer is full. And you have another type of thread which is called consumer, which just keeps consuming items from this buffer until the buffer is empty. So this is the question set up. Now, suppose you are supposed to implement producer. How are you going to implement your CV? This is the example of CV that you can see in almost every OS textbook. So the basic idea of producer is that I produce item. I want to put an item in the buffer unless the buffer is already full. In that case, I want to wait. So the condition for the producer I want to wait is the buffer is not full. It has some capacity, right? So now think about how you will implement the producer. So first you get some item. I don't care how you get that. You have an item you want to put this item into the buffer. Now you want to acquire the lock for the buffer because this buffer in this case is the shared resource. Before you try to put an item in it or you try to get an item from it, you should acquire the lock to protect the access to the buffer. So lock for this buffer. Then you do what? Can you just put item in the buffer? Why not? Do I care if it is empty? Why? As a producer, what do I care? If it is full, then I cannot put item in it. If it is empty, I don't care. So here the condition you check is suppose this is the current size as long as the size equals to the max, which means the buffer is full. I do what? CV weight. The first should be CV and the second should be lock. Now you see a pattern here when we use CV. We always use the CV in a loop. We want to keep checking that condition. So I acquire the lock for the buffer. I check the condition I care. In this case, the condition I care is if the buffer is full. As long as the buffer is full, I keep wetting here. Remember that in CV weight, what do we do? Release the lock. Why do we release the lock? This thing? CV weight is a function. The first argument is variable called CV. Suppose we already have a CV declared before that. The second argument is the lock. In this case, I name this lock as a buffer because the lock is supposed to protect the buffer. Remember what we do in CV weight? You first release the lock. So if the buffer is full already, then allow the consumer to consume item from the buffer and make it not full. I release the lock. I sleep. After I wake up, I acquire the lock. So again, as far as this producer concerns, before CV weight, I have the lock. After CV weight, I still have the lock. Now I go back to the condition and check if it's still full. If it's full, then I keep wetting here. If it's not full, then great. I put the item in the buffer, release the lock, then I return. This is the logic for the producer. Now, any questions before we go to the consumer? So the logic of consumer is quite similar. So suppose I have a function called consumer. The first thing I do is acquire the lock for the buffer. Then what's the condition I care about as a consumer? If it's empty. So while the buffer size equals to 0, I call CV weight. So here the condition changes. And the consumer, I don't care if the buffer is full or not. I only care if the buffer is empty. As long as it's not empty, I can consume. Otherwise, I cannot. So the condition here I care about is if it's empty. Similarly, if it's not empty, then great. So I consume one item from the buffer. I release the lock. Then I'm done. So these are two examples of how you can use semaphones to let a thread wait for a condition to happen. Now you get a more sense of what is called the condition variable. The condition variable itself doesn't have any conditions. The condition is outside. It depends on what you check on when you call CV weight. Oh, CV? Yeah, because this function doesn't know which CV you want to wait on. You have to pass the CV as a first argument saying, I want to wait on this CV. It comes from earlier on, but at the very beginning of the program, you might declare CV equals to what? So you create a CV in other places. Here, we assume you already have a CV for the buffer or for the condition. Any other problem? So here you can imagine CV just as a weight channel. But the weight channel has something to do with an actual lock, which it does something with the lock. We first release the lock, then sleep, then acquire the lock again. Any other questions? Does everybody understand how to use a CV property? You may need to use CV in later assignments. For example, in assignment one, in reader and writer lock, you may want to use CV. Or in the while mating problem, you may also want to use CV. So CV is very useful for these kind of synchronization problems. So no questions? Okay, let's move on to reader and writer lock. So you can imagine reader and writer lock as a special type of lock, which allows multiple threads in the correct section, as long as this thread does nothing but read the shared resource. As long as this thread doesn't change the status of the shared resource. Intuitively, that's correct because, for example, if I have a large database, you can imagine this database as all the books in the library. So because it's a shared database, before everybody access the database, they have to grab some kind of lock to protect from each other. Suppose you are students. What you want to do is just to query the status of a book. You just want to read data from the shared resource. On the other hand, as the manager or the library, what she or he want to do is update the status of the book. She want to write to the database, and what you want to do is just to read from the database. So intuitively, multiple students should be able to read the database at the same time. It doesn't make sense to let you guys queue up and wait for each other because what you want to do is just to read. You won't do any harm. But once the library or once the manager comes into the query section where she want to update the database, then nobody should be accessing the database because you may see inconsistent values. That's a typical example of where you can use reader and writer locks. The idea is you allow multiple readers in the query section, but you only allow one writer in the query section. That's the concept of Spinlock. Speaking of implementation, what kind of problems you need to consider or what kind of questions you need to ask yourself? A most important question is what's the condition for a thread? You can allow a thread to enter a query section. In RWLockAquireRead or RWLockAquireWrite, when do you allow that thread to go through that function or return from that function? Obviously, we have some web channels inside RWLock where you want to put the thread into sleep. So when do you let the thread pass the acquired function? For readers, what's the condition when a reader can come through the acquire and enter the query section? No writer is holding the lock, correct? No writer is holding the lock. That's partially correct. If there is no writer in the query section, a reader should be able to come in. Like I said, that's only partially correct, but for now, let's just take it as correct. So for readers, no writers inside can come in. For writers, what's the condition? No readers inside, what else? No writers. Basically, nobody inside. So if readers inside, you cannot come in. If there are other writers inside, you can still not come in. Now let's go back to the condition for readers. You said as long as there are no readers inside or as long as there are no writers inside, I can enter the query section. That's what creates the starvation problems. Let's see how we can start a writer if you use that condition. So suppose this is time. We have a reader comes in, wants to read, no problem, nobody inside the query section, and it comes into the query section, enters the query section. Now, later on, before this reader finishes, a writer comes. Can this writer come in the query section? No, because there is a reader inside. So before this reader finishes, again, another reader comes in. Can this reader enter? Yes, because there are no writers inside. Now, can this one enter the query section? Can this one enter the query section? And it keeps going. What about this poor writer? If I have readers come and go, this writer will be starved. That's what we want to do. We want to stop these guys as long as there is a writer before you. That's what you want to do in reader and writer locks, to prevent writer's deviation. Previously, we say as long as there are no writers inside, this reader can enter. That's not correct. What's the complete condition for this reader to enter? No writers inside or waiting. That's the complete condition for this reader to decide if this reader can enter the query section. That's basically the only trick for reader and writer locks. Any questions? Yeah. So what you are saying is we have a writer, we have a reader waiting here, we have another writer. You mean the writer keeps coming and going, starved the reader? Is that the case? That's a good question. So if this writer is inside the query section, both readers and writers are waiting for the query section. When these writers are done, everybody should have a fair chance to compete for the query section. You cannot always give the priority to writers. As you said, you will stop readers in that case. So different from the previous case, when you have a writer inside the query section, these guys, suppose these are writers, they cannot enter the query section anyway. They have to wait on this other block. So when this guy is done, he needs to wake up somebody. So these writers need to wake up somebody waiting on this other block. He should wake up everybody instead of just wake up the writers. If you just wake up the writers, you are giving priority to writers, then you will stop the readers. Everybody get what I'm saying? So when this writer is done with the query section, everybody who are waiting for this other block, whether it's a reader or a writer, should get a fair chance to compete for the other block. Suppose the reader grabs a log, then what will happen? Then these writers need to wait again, wait any longer before the readers are finished. That's a good point. You don't want to stop either the reader or the writer. If you think about it, actually it's two different conditions. So if a reader is inside, and a further reader wants to come in, that reader wants to check if there's a writer waiting for it to avoid starving that writer. If there's a writer inside, and both the reader and the writer are waiting for it, then it's a different condition. You should differentiate these two scenarios. Any other questions? No? So this is for the reader and the writer's logs. Now we have about 20 minutes to talk about the synchronization problems. Everybody look at the synchronized problems. Do you know what they are? So the first problem is called well-metting problem, which basically says that we have a bunch of thread which falls into three categories. One category is male, another is female, and another is matchmaker. And well-metting needs all three kinds of wells. So you are supposed to use either CV, or log to coordinate between the wells to make the matching process smooth. That's basically what it says. So what's the problem here? So what's the synchronization problem we are trying to solve here? Well, well-metting requires all three of them, male, female, and matchmaker. You want a match happening with the presence of all three of them. In the output, if you see male start, female start, male end, female end, then you would know it's wrong, but in that case, the male and female finishes the process without a matchmaker. That's how you can use your eyeball to tell the process is not correct. Otherwise, you really don't know because the male number one could be together with female number two or with any females. All you can check is that the matching finishes with the presence of all three of them. How many of you have started working on this problem? One, two, three, four. So you don't really know what I'm talking about. So anyway, it's not very hard just to remember that you need all three to make the mating happen. I won't talk about... I won't teach recitation this Friday, so I don't have a chance to further explain the problem after you all have read it, but that's basically the idea of this problem. The second one is more interesting or more easy to understand. By the way, how many of you have read the second question? The intersection... Just a riddle. Don't have to do any coding of it. Just know what the problem is. Still less than half. Anyway, so the second question says like this, suppose we have an intersection and that intersection is a four-way stop. Basically, you will see a sign like this with all the way here. All the way stop. It's a two-lane road and what you want to do is suppose you have a bunch of automatic cars which is driven by computers and you are supposed to design programs for these computers to pass these four-way stops correctly. Correctly mean no car crash and everybody follows the rules. For example, we have four directions, 0, 1, 2, 3. Suppose a car is coming from this direction and wants to go straight. Now we're naming this quadrant accordingly. I labeled the quadrants. This should be 0, 1, 2, 3. This quadrant is 0, 1, 2, 3. So suppose a car comes from this direction and wants to go straight. Which quadrant this car will come across? 2 and 3. What about turn right? Only one quadrant, 2. What about turn left? Instead of 2 and 0, you should go to 2, 3 and 0. 2, 3, 3. You need to acquire three quadrants to pass this quadrant section. So the requirement of your program is first of all, safety. No more than two cars should be in the... Actually no more than one car should be in one quadrant at any time. Otherwise you have an accident. That's number one requirement, safety. Basically you have four quadrant and each of them is a shared resource. Basically you have four pieces of shared resource. That's number one requirement, safety. It's exclusive access. Number two. Number two is efficiency. What's the first solution you come up to hear about this problem? Anybody? No, what's the first... No, what's the very first idea you come up if you want to solve this problem? No, the commuter is not that smart. Yeah, exactly. I have one big lock for this whole intersection. In that case it's safe. You can't park in any of the quadrant but it is correct. But it's not good in the sense of efficiency. Suppose I have two cars coming into opposite directions. One from here, one from here. They should be able to cross the intersection at the same time. It's different than a real four-way stop. You can start at the same time if they don't collide. Or in the most extreme case, I have four cars coming at four directions and all of them want to turn right. They should be able to make the turn at the same time instead of all of them waiting for each other. This is what we mean by efficiency. Actually, in the grading, the grader will try to identify if you use one big lock solution and take some points for that. So don't even try that. Or if you don't really have time, you can use it. It's correct. It just works very well. This is the second requirement, efficiency, which basically excludes the possibility where you have one big lock for each quadrant. The third requirement is liveness. What do we mean by liveness? Two cars coming from... Suppose one car comes from here and he wants to turn left. Another car comes here and he also wants to turn left. That won't create a dialogue. Anyway, let's make the scene much more interesting. We have four cars coming at each direction and all of them want to turn left. This car comes here, enters this quadrant because nobody is inside. This car enters here, this car enters here, and this car enters here. What's next? Everybody gets stuck. Everybody gets stuck. There's no way out. Unless you're human, you're smart, you back off and let somebody else pass. But for computers, they're dumb so they don't know how to back up. They get stuck here. What you want to try to avoid is exactly this kind of situation. What's that? It is called dialogue technically. But you can... Yeah. So this is the requirement for this intersection problem. You can use any of the CV, semaphore, lock to try to solve this problem and try to enforce the synchronization or the coordination between the cars. Any other problem about this question? Yeah? No, you don't want your spin lock after lock. Once you have a lock working, you always want to just use the lock you implemented. Any other questions? Everybody know how to solve this problem? Well, I didn't actually tell you that, but you should have some idea. So first of all, what kind of primitive you want to use? You have to use one of lock and semaphore to ensure that kind of mutual access, mutual exclusive access. Lock is ideal fit in this case because it doesn't make sense to have a semaphore for each quadrant with an initial count one. That's basically a lock. So you should have some lock for each quadrant. And what else? Yeah? Maybe... Yeah, you may use some condition variable say the condition could be all the quadrant on my parts are empty. That could be one condition. Or... What's that? That also depends on your direction. The parts you are going to use depend on the direction where you come from. So minimum, you should use a lock and how to use them, you need to figure out. But whatever solution you come up with, check these three conditions. Make sure you're taking care of them. Yeah, what condition? Rest condition? If you implement the lock correctly, you won't have any rest conditions. Right? No. Yes. This is just a kind of simulation. Of course, in practice, it's not just to go over there instantaneously. We assume that. If you look at the code in the function call, it tells you where this car comes from and where this car is going. So you know exactly which quadrant you will use. Okay, I guess that's it. Thanks and good luck with the assignment.