 I'm out to cover today, so I want to just get right started. Today, what I'm going to try to do is wrap up the content that you'll need to do assignment one, which is do two weeks from last Friday. And then on Wednesday, we'll go on and go back to our discussion of the system call interface and talk about the rest of the process life cycle. So today, we're going to talk about a couple of synchronization primitives and then try to use those synchronization primitives to solve a couple of classic synchronization problems. Then towards the very end of the class, we'll have a little bit of a discussion about how to choose the right primitive for approaching a particular problem. But this is something that will be covered in section and through your experience with assignment one this week in more detail and next week as well. So if you don't have a partner, you need a partner. I think, hope I explained with enough accuracy to partner protocol for this class. So you should be working on assignment one right now with your partner. If you're not doing that, you need to find a partner. If you are the one person in this class, we have an odd number of people who are enrolled. If you are that one person who does not have a partner, please contact us. We will probably get seven emails like that, and we're going to partner up six of you. But there will be one person left over. That's inevitable. We're an odd number this semester, at least so far. But please don't feel awkward about this. You really need to have a partner. And if you don't, you need to fix that as soon as possible. Because once you start submitting the assignments, you're going to need to be working with that person. OK. So I posted this on Discord. But just let me introduce the new protocol for discovering errors in the assignments and the website and other things. So this is quite simple. If you find, so all the sources for the website and the assignments and including the tool we're going to start using to grade your assignments are all up on GitHub. So it's very simple. If you find a typo, or if you think that you can add some clarification to the assignment that's useful, we will give you some amount of extra credit for the class, up to like 5% for the total for the semester. OK. The caveat here is that you cannot just email us and be like, there's something on the assignment that doesn't make sense. You actually have to fix the problem in most cases. If it's something really strange with one of our tools that you don't really understand, if you can identify it and you can show us how to reproduce it, then you'll get some credit. But particularly for things on the assignment, I've had a couple of people that have said, there's a typo here or there, whatever, fix it. Fix it. Send us a pull request on GitHub. And you'll get some small amount of credit. And that amount of credit is completely at the discretion of the core staff. So we will do our best to be consistent about this. But finally, please don't let this distract you from the actual assignments. This is just something to encourage you to, if you see something wrong, say something so that we can fix stuff and improve things for the future. Any questions about this? OK. Next thing. So we posted all the assignment deadlines. I'm in the process of working on getting assignment two out there. Assignment one was released last Friday. So here's how this is going to work. So you have two weeks for when we told you the assignments were due, which was last Friday. So you have two weeks to complete assignment zero and assignment one. Then you'll have three weeks for assignment two. That essentially ends up right before spring break. So assignment two is due to the Friday before spring break. If you have plans to go somewhere exciting and fun and you're going to leave on Wednesday, then your due date is different than the rest of the class. Then we'll have the spring break off. The next week, we'll have the midterm on Wednesday. So we won't count that as part of the time you guys spent on assignment three. And then we've divided assignment three into three pieces this year. This is an experiment. So every year I've taught the class, we've tried something different with deadlines. The first year, we had kind of normal deadlines. The second year, we had no deadlines. Then last, I think two years ago, we had one, two deadlines. Last year, we had more deadlines. And this year, I guess we're trying even more deadlines. So I don't really know. I mean, no matter how we do this, it seems that people do not start the assignments early enough and struggle to complete them. But this is our latest experiment. And regarding time management, so how many people have started on assignment one? OK, so that's good. There's a couple of hands up. At this point in the semester, you guys don't really have a handle for how difficult these assignments are. And maybe they won't be hard for you. I don't really know. They were hard for me back when I took the class. And students usually find them challenging. So my suggestion is, if you think the assignments are easy, then just start them now. I mean, don't wait till Thursday. Just do them now, and then they'll be done. No problem. So if you really think you're going to be able to complete assignment one in a couple of hours, then why don't you just set aside those couple of hours today, and then you'll be done. No problem. So this is what I encourage you to do. If you think the assignments are easy, start them a week before, and then you can just kind of hang out and relax and everything like that. That way, if you find out that they're more difficult than you anticipated, then you will have more time. I don't anticipate pushing any deadlines this year. You guys know them. The materials are up. We have plenty of time for you to do the assignments, so we're going to be pretty inflexible about things. And there will be heavy penalties for use of the solution sets as we've described in the past. All right. OK, any questions about administrative stuff? OK, happily, this is getting down to a dull roar. All right, so today, where we left off last time was we had essentially designed a spin lock using low-level hardware primitives, and that spin lock was providing us a starting point for fixing some of the synchronization problems, the race condition that we had discovered in that piece of bank balance code. So if you remember, I think we got to this right in the class. So locks are core synchronization primitive, and they're used to implement critical sections and generally to guard access to shared state and shared data structures. So in cases where you can have multiple threads accessing the same piece of state, and in cases where some of those operations need to be atomic or there's certain things that a thread can't be interrupted in the middle of or states that the data structure should not be seen in, the lock can make sure that this happens. The lock interface is quite simple. It has two commands. You guys could implement this for assignment one. It's acquire and release. It's kind of what you'd expect. You lock a resource while you are busy using it, and then you release the lock when you are done using it. The spin lock is what we discussed on Friday. So the spin lock is a form of a lock where, remember, there were two ways that I could wait for a lock to become free. So if I try to acquire a lock and the lock was busy, there were two ways to wait. What were those ways? One way was busy waiting, sort of repeatedly checking something about the system to see if it's changed. And in the case on Friday, what we talked about is repeatedly hammering this memory location using a test and set or some sort of low-level hardware primitive to see if it's been updated. And that will work, right? And a spin lock describes the process of acquiring this particular type of lock, which involves busy waiting. And spin locks are not used normally on their own to acquire resources, except in a couple of cases, which I'll describe in a second. But they're frequently used as a building lock for creating more complex and more useful synchronization permits, OK? So we went back to the, so now let's imagine that I've put this lock behind an interface. Last time, I was actually implementing a kind of a lock directly using this test and set operation. And now what I'm going to do is just wrap my lock behind an interface. It's nice. It's very similar. We give you guys to implement for assignment one. So at the top, remember, these three lines were in my critical section, the lines where I'm manipulating the shared variable, the balance. And so what I need to do is acquire the lock before I enter the critical section that ensures that nobody else can get in there. I know that I have exclusive access within that section to the shared variable. And then when I'm finished, I release it, OK? So what has to happen in order to meet the property of mutual exclusion, which is why we created this primitive? What has to happen if I call lock acquire while another thread is inside the critical section? So let's say there's 10 threads that are trying to modify the balance all at once. What happens to the first thread that calls lock acquire? What does it do? Keeps on running, right? It starts to modify the balance. What happens to the second, third, fourth, fifth thread while the first thread is inside of it? Yeah, average. So it has to wait somehow, right? The only way that we've talked about waiting so far is by spinning. But a more useful way, potentially, to wait for a resource to be available is to have some support within the operating system for a thread to identify an event that it's waiting for and to be restarted when that event takes place. And if you look at your kernel, you have support for this already. Yeah, question? Yeah, so interrupts play a role in this, right? We'll come back and talk in a few lectures about how interrupts play a role in this, right? So interrupts give the operating system a chance to retake control and to start and stop threads. That's the way that the operating system, that's the underlying hardware mechanism that allows the operating system to create this illusion of concurrency by starting and stopping threads rapidly. But in general, in order to sleep and wait for something, I mean, sleeping is not hard to think about. Just means that the thread is no longer executing instructions. But what it needs is a way, needs two things. It needs a way to identify the thing that it's waiting for. And then it needs a way for the operating system to restart it once some event happens. In this case, how would I event, how would this, how are these threads identifying the thing that they're waiting for? What identifies that? What's that? I mean, it's the gua wallet lock, lock variable. The OS doesn't really know what you called it in your C code. What would be a unique property that that would have across the entire system? Yeah. What's that? Oh, it's closed. It's not a function, right? The lock acquire function is going to be shared by a lot of things. But what address is unique here? The address of this lock data structure, that has to live in memory somewhere. And that address should not be shared by any other structure. So that becomes a unique key that the threads use to identify the thing they're waiting for. So what we write here is we write lock acquire, the address of this data structure, what the kernel eventually sees is I would like to wait for this variable is this hex string that identifies a memory address. You guys will see how this is done in OS 161 because this is sort of based on the primitives you guys will need to use to implement locks and condition variables, which we're going to talk about in a second. OK, so I just pointed out there are two ways to wait. So one way is to wait actively, rechecking the thing where the thread is doing the checking. And the second one is what I just described. I wait passively. I need to tell the kernel what I'm waiting for. And the kernel needs to sort of be able to identify when that thing changes. And then when that happens, the kernel is going to wake me up and say, you know, you told me that you wanted to wait for this hexadecimal memory address. Well, this hexadecimal memory address just changed. Or actually, I shouldn't put this way. Somebody just notified me that this has changed using another part of the API. And now you get to run again. So this is sort of explicit sleeping. Now there are cases where spinning can actually be the right thing to do. Can anyone think of any of them? Sort of, it's maybe a difficult question for you guys to answer right now, because we haven't really talked about threading and context switching. Yeah, way up in the top. Yeah, OK, somebody looked at the slides. I like that. So OK, so first of all, let me point out something. Again, back to this weird world where there are actually single core systems. On a single core system, it never makes sense to spin waiting for a resource. Why not? Yeah. Yeah, right. And what's required for that thing that I'm waiting for to happen? Think about the bank example. Some guys outside the critical section waiting to get in the critical section. What is he waiting for? Another thread to get out of there. So if I'm sitting there burning cycles, all that is just time that the other thread cannot run. Does that make sense? So on a single core system, again, of which there are like two left in the world, this is never a good idea, because spinning always prevents the thread, some other thread, from making progress. And that's what I need to happen. That's the only way that the decision is actually going to change. But there's an interesting trade-off here between the length of the critical section and the overhead of a context switch. And again, we haven't talked about a context switch yet, because we actually haven't talked about threading, which we will when we talk about how the CPU is multiplexed. But it turns out that there's an overhead required for the operating system to stop one thread and to start another thread. You might have anticipated this. In order to stop a thread in the middle of doing something, I have to save a lot of information about what that thread was doing. So when I restart it, it can't tell. So if you think about the C programs that you write, when they run on a computer system, each one of those threads is being constantly stopped and started over and over again. And yet you can think about it as if it's executing line by line in some sort of sequential way. And the reason is that the operating system is very carefully when it stops your thread, saving everything about the state of the world that thread might be able to observe, and when it restarts it, it has to restore all that. So that creates overhead. So there's overhead for switching back and forth between two threads. Now, if I sleep waiting for something, I'm forcing the context switch to happen, because when I go to sleep, the operating system is going to schedule something else. So if I think that the critical section is short, and if you write the code, then you know this. So if the critical section is quite short, then it's possible on a multi-core system that it's actually going to be more efficient to spin just for a couple of cycles, right? Let's say my critical section only has a couple of instructions in it. So I have a diagram here, right? So imagine if the critical section is short, then thread one was inside the critical section, and thread two tried to get in, and thread two went to sleep. So now there's all this context switch overhead that the kernel has to perform. And actually, by the time the context switch is over, thread one is long gone. So it would have been smarter for me to just wait, bang on that critical section for just a minute, and get in as soon as he was done right here, right? However, if the critical section is long, then it wastes a lot of resources to wait actively. So this is the trade-off. The critical section is very short, and this is why spin locks are frequently used to implement other primitives, including locks that sleep, because there's a small part of that sleep-alk implementation where it's just executing a couple of instructions, just reading and writing a few, from few locations in memory, and that can be protected with the spin lock because it's very fast. It doesn't sleep, it doesn't do any I.O., you know, it's just, you know, you can almost look at it and convince yourself that this is only gonna take a couple of, you know, cycles to execute. Maybe a little bit of delay for the memory reads and writes to happen. Okay, so I think I already, I think I got ahead of myself today. So the kernel, in order to sleep, the kernel has to provide this functionality that allows the thread to sleep and be awakened based on some key, where the key is normally the address of some data structure or some unique value that threads agree on. And this is what you can imagine, the, you know, the interface here is to this particular piece of kernel functionality. Again, you guys will see this again when you do assignment one, because you will have to understand how this is done in OS 161. This is not the name of, this is the interface, this is essentially the interface provided by OS 161, but these are not the right names. It was, okay. And it turns out you can actually do the same thing in user space. So they're user space libraries that implement things that are quite similar to this function. Okay, any questions about locks at this point? I'm gonna go on and talk about a different synchronization permitted. Yeah. For the user space libraries, are they just kind of like, almost like an API boundary where they're like implementing their own kind of like, to make the kernel what else to be implementing? Yeah, so we'll come back to this when we talk about threading. So it turns out that a lot of the kernel threading abstractions can be easily be implemented in user space. And for a long time, there were a lot of kernels that actually didn't provide any support for having multiple kernel threads within a single user process. So from the perspective of the kernel, every user process only looked like it had one thread. Now, in order to achieve multi-threading on a system like that, you have to use a threading library. There's something called pthreads. It's been around for decades. The go runtime provides support for really lightweight threads. There's so much ways to do this. What big change that I keep alluding to, do you think caused a lot of kernels to start providing kernel support for multiple user threads within the same process? It's a big technological shift that's been going on over the past 10 years, yeah. The web. No, good guess though. I can do web really well using user threads and sometimes better. So if the kernel only thinks that a user process has one thread, what inherent limitation does that create that would be very problematic on a modern system? Yeah. You can only use one core. So if my kernel thinks every process on the system only has one core, one thread, then it's only gonna get scheduled on one core at a time. It might be that that process has a threading library internally that's created like 100 threads, but if the kernel doesn't know about them, it can't schedule them on multiple cores. And so when multi-cores started to take over, this became untenable. Yeah, yeah. Python has some serious problems with multi-processing. Yeah, anyway. Yeah, that's one of the reasons that you get frustrated with Python very fast, especially when you're doing IO. Okay, any other questions about locks? You guys will have a chance to implement locks. You guys will have a chance to use locks for assignment one. Okay, so locks are, on some level, you could argue one of the simplest synchronization primitives. The interface has two calls to it and you can really kind of understand what they're designed to do, which is to serialize access to sort of shared data structures to protect critical sections. Now on some level, you can imagine that locks sort of permit a very limited form of communication between two threads. So when a thread that's inside the critical section calls lock release, it's a sign to a thread outside the critical section that's trying to get in that there is now the critical section is now available. But locks are not really themselves designed to be a signaling mechanism. Locks are used to protect parts of code and provide guarantees about how different parts of your code will interleave when they're being used by multiple threads. Okay, but there's a lot of other types of information about shared state that threads might want to communicate to each other. So you can imagine cases where a thread in a program wants to be notified when somebody clicks on a button, when there's some data in a buffer that needs to be processed. So there's a much, in order to get multi-threaded programming to work well, there's a much richer variety of types of communication that we might want to enable. And one of the ways we do that is through something called a condition variable. Now condition variables are kind of interesting. When I started teaching this class, I guess I could admit this now because I think everybody I had in class a couple years ago was gone, except for some of my RAs. I'm not even really sure I understood condition variables completely. But it turns out they're actually super simple, okay? A condition variable is really just a thread safe communication mechanism. So by thread safe, it means that a condition variable provides some guarantees about how signals are delivered from one thread to another. We'll come back to that in a second. So the CV interface is CV weight. So when I CV weight on a particular condition variable, what I'm saying is I'm waiting for something to happen, typically for that condition variable to change, but frequently CV weight is used in a loop where it's checking some other condition, right? So I'm checking how many objects are in this buffer. I'm checking whether or not a particular thing is true or is changed. CV notify is used by the thread that changes something to notify other threads that that thing has been changed. So this gives me a way to wait for something to happen and for the thread that actually causes that thing to happen to tell other threads that it did happen, yeah. So that's a great question and the answer is not much, right? So there is a very close relationship between this interface for condition variables and the thread sleep and thread wake interface that we just described. So when you guys finish your condition variables for assignment one, don't expect to have written a lot of code. If they're only like three or four lines for each one of the calls, you're probably doing something right because a lot of the functionality is already there. There's really not much to do. Does that answer your question? So the condition, so what am I waiting for, right? So condition variables are really just a signaling mechanism. The condition is represented outside of the condition variable and this kind of makes sense because otherwise you can imagine like these really complicated condition variables that we're expected to handle in every case. So what threads do, and I'll show you some code in a minute, we'll go through an example, is they typically check the condition and then if the condition is not, does not allow them to proceed, they call CV weight and wait for it to change and then when they wake up, they check the condition again and we'll see why that happens, okay? So the condition variable, because it's combined with observing some state about the world, can convey a lot more information than a lock. Again, it's really just a signaling mechanism that allows me to communicate arbitrary things between two threads and to do that in a way that's thread safe. So let's go through an example here. Am I getting right into the example? Okay, yeah, okay, yeah. So we're gonna do an example in class today. The example is a bounded buffer, producer, consumer problem. So imagine that I have threads that are producers of items into the buffer, threads that are consumers of items out of the buffer and those threads want to coordinate so that a couple of things are true. I wanna make sure that when there's data in the buffer, there's an active thread that's trying to consume it and when there's space in the buffer, any thread that's trying to put stuff into the buffer can proceed. The buffer is fixed size. So this creates conditions because of a fixed size buffer where the producers have to wait because there's no space in the buffer and of course if the buffer is empty, then the consumer threads have to wait as well because there's nothing to consume. When you're using a condition variable, it's usually really helpful to identify the conditions. So in this case, the buffer has three states. It can either be full, in which case the producers have to wait, it can be empty, in which case the consumers have to wait, or it can be in neither, it can be in between, in which case nobody has to wait. Right, so three different states, two different types of threads. So you might be thinking up until this point, why are we talking, there's no synchronization. I keep saying, so this can happen in a thread safe way, but that's just like professor mumbo jumbo, right? Like what does that actually mean? So what it means, so why are condition variables a synchronization mechanism? Well, I have multi-threading here, right? I've just said I have a bunch of consumer threads and a bunch of producer threads. I also have shared state. What's the shared state? The buffer, right? That's what everybody's trying to access. It's shared by all the threads that are contributing or removing items from this buffer. So the reason why condition variables are considered a synchronization mechanism is they're frequently raised conditions that are introduced when multiple threads are trying to access the same state, okay? So let me show you an example. So let's say that thread A comes along and for some reason on this slide I realized today, I switched to Python pseudo code, I don't know why. Anyway, I'm sorry, I think that ends soon. So if the buffer is empty, and let's say that, remember, we have to relax all of our assumptions about how threads are run. Threads can be started and stopped at any time. So at this point, let's say that thread A is trying to consume from the buffer. If the buffer is empty, what does thread A need to do? It needs to wait. It needs to wait until there's data in the buffer, right? Okay, so it checks the buffer, and let's say the buffer is empty. Unfortunately, right at this moment, thread B comes along and puts stuff into the buffer. Unfortunately, the next thing that thread A is going to do is call sleep. So now, and you guys, I'm sure this will happen to you when you do some of the assignment one synchronization problems, you have, this is essentially a deadlock because thread A is sleeping, and let's say that thread A and thread B are the only threads, they're the two lonely threads in the world who are gonna use this bounded buffer. So all, like thread A's whole goal in life was just to receive that item from thread B, that's it, and thread B's only goal in life was to get that item to thread A. So thread B accomplished its job, it put the item in the buffer, and it tried to notify thread A, but the problem was thread A wasn't actually sleeping yet when it did this, and by the time thread A gets to sleep, thread B is long gone. So when is thread A going to wake up? Never. So this is where synchronization comes in with using a condition variable, which is that, remember, the condition can be arbitrarily complicated, it's just arbitrary C code, and I need to make sure the condition doesn't change between the time I check it and the time I sleep on the condition variable. If it changes in between those two times, then I may sleep forever. Or in other cases, I may miss some event that I needed to. Yeah, check that out. All right. Okay, so I'm gonna go through an example with condition variables. I just wanna point out that I took the semaphore stuff out of today's slides because we're just a little bit behind on time. So those will be covered, you'll see those in assignment one, and I'll ask Jerry and Ali to cover them in recitation this week. Semaphores are not always that useful, okay. So let me talk, I'm gonna do two examples now for the remainder of class. So the first one is gonna point out a problem with locks, and then we're gonna come back and actually do the bounded buffer producer-consumer using semaphores, okay? So locks are super useful, but locks have a problem that you will likely encounter. If not, during this assignment, probably for assignment two or assignment three. So locks predicts access to shared resources, and frequently threads are in a case where they actually need multiple shared resources at once. So in order to do something safely, I need to use this resource and that resource. Both of those resources are protected by a lock to make sure that they're consistent across shared modifications, and I need both, okay, project. Yeah, so this is a good point. It's not, so when you decide to use locks, I mean, look, the safest thing to do with a lock is when you have some state that shared, lock it. Create a lock for it, and make sure that everybody who accesses that state acquires and releases the lock. Now, I have a sad, what's that? Oh, sorry, I have something sad to share with you, which is that unlike some other languages that are better than C, C provides absolutely no help with this, okay? So, I've had people come in for help officers and say, well, I have a lock, and I'm about my code, I'm still having this problem that's crashing or whatever. Just having a lock does not automatically protect a shared resource. In some languages, it's like Java, you can just say, synchronize, and Java, I guess, will figure out what to do for you, probably badly, but whatever. So, the C, not so much, okay? So if you declare a lock and don't use it, it has no impact. Now, okay, look, you're not that dumb, I'm not implying that you're that dumb, but what people frequently do is they declare a lock, and they use it in like three out of four places, right? So you, and then you ask them, well, show me all the places where you modify this data structure, and it's like got one, got one, got one, whoops, right? And all it takes is one place. Now, when you go on and you start contributing to Linux or something like that, there are certainly cases where you can get away with not locking state if you are absolutely certain that what you are doing is safe. That frequently happens when you are only reading the data structure. So let's say I'm just reading from a data structure and I know that my reads are safe, depending on the operations that are going on. Now, in certain cases, this doesn't work either. If I'm trying to walk a linked list that's being modified by multiple threads, that will just go terribly wrong, even if I'm just trying to walk it and somebody else is trying to modify it. So it doesn't always work, right? But yeah, I mean, the safest thing to do is to lock when you access a shared resource, whether that access is read only or whether you're modifying it. Does that answer your question? Okay, so, but threads again, may need to view or edit or modify multiple shared resources. Oops, so let's go through this example. So let's say I have two resources and here's sort of a canonical example. Thread A runs, thread A and thread B are both trying to gain access to resources one and two. Maybe I'll come up with cuter names for these in the future, but anyways. I have two animals now, so that would work. Okay, so thread A runs and it grabs the lock for resource one. Now I have a context switch, thread B runs, it grabs the lock for resource two. Does anyone see how this is going to end? Yeah, so now at this point it doesn't really matter what happens next. I'm in trouble because someone is going to try to get the lock for resource two and sleep and someone's going to try to get the lock for resource one and sleep and now when will these pair of threads wake up? Never, they are both, we have a cycle in our waiting graph. I have thread one waiting for thread two, thread two waiting for thread one. Okay, so this is not good. This is something that's referred to as deadlock and deadlock can be a function of use of synchronization primitives and especially sort of over-aggressive use of synchronization primitives, but usually just incorrect use. So deadlock occurs when due to trying to acquire some sleep lock, I have a cycle in my dependency graph and a bunch of threads will never make progress. Now you actually don't even need more than one thread to deadlock. How can a single thread deadlock try to acquire the same lock? Now this isn't as weird as you might think it is. So you might think, well this is just dumb, why would I try to acquire the same lock twice in the same thread? How does this happen usually? Now a lot of locks will protect against us and they'll just blow up or panic or something if you try to do this, but it still happens. Why does this happen? I mean isn't it like, can't you read your code? You've got four lines there. Line one is acquire lock foolock and then line four is acquire lock foolock? Yeah. Oh that would be awesome. That's maybe even worse. So don't acquire a lock in a loop. Now how would this happen? You write your function and it acquires a lock once and then it's still, you see some self deadlock or a panic because it's trying to acquire the same lock. Yeah, you don't release it. That's true, so that could happen. Let's say you pair your acquires and releases the way you're supposed to. Exactly, so you're working on assignment three and you call your partner's function and your partner's function acquires that same lock. So this frequently happens when the code is factored in a way multiple threads need the same lock. Now there's ways around this. There are nice synchronization primitives that will do the right thing here, which is you can have a recursive lock. I would encourage you guys to implement a recursive lock if you want to. Actually they're not that complicated to build. Recursive lock just keeps track of the number of times that it's been locked by the same thread and then make sure it's like a push and pop, right? So you just keep a counter. You say, okay, you've locked me 10 times. When you unlock me 10 times I'll actually unlock. And that can actually be pretty useful in cases particularly where you have lots of modularity in your code and you need to have two functions that can lock the same resource regardless of who runs first, right? So if foo calls bar it works and if bar calls foo it works and if you call foo and bar separately it works. So this can be potentially useful. It's not something that we give you. Okay, I just said all this. It's amazing. Wish the slides would just like keep up with me. They should be listening. All right, so conditions for deadlock. So there are a series of conditions. Now when you encounter a deadlock in your code this is a good way to figure out what's going wrong. Because there's a series of conditions that have to be satisfied for a deadlock to take place. The first is you need to have shared resources. So in some cases you can fix deadlocks by just refactoring resources so you don't need to lock them. And that's pretty awesome in general because locks introduce overhead. Locks can make things sleep and wait for something else to happen. So if you have a shared resource that doesn't actually need to be accessed in a shared way you can break it up into multiple pieces, you're gold. And this is something that kernels have done for years to try to improve performance. It's to say, you know what? We don't need this big table. Let's just break it up into a bunch of smaller tables and that'll work just as well or better because I don't have to synchronize it. No resource preemption, meaning that the system can't go over to thread A and say, I need that lock back. Sorry, I know I said you could have it. And you can imagine why this is the case because this gets terrible. I mean, yeah, okay, maybe I create some way so that when I lock the lock I provide a function that gets called if the lock has to be taken away from me but it's very hard to get that right. I mean, the idea of calling lock a choir and then proceeding when that lock is acquired is very natural. It's very simple and providing support for being able to actually remove locks from threads is not very good. If I could do this though, how do I use it? Let's say I have this feature and I have a cycle of locks. What do I use it to do? Let's say I go back to the example before I have thread A waiting on thread B, thread B waiting on thread A. They're both holding a lock that the other one needs. Let's say I have some mechanism where I can forcibly take the lock away. How do I apply it to that situation in order to eliminate the deadlock? What's that? Oh, no, well, okay, so I have to be able to identify the cycle, so that's a good point. And so I need some sort of deadlock detection that where the kernel can see, oh, I have a cycle in my graph, I have a problem. But once I do that, I just break the cycle. I just take the lock away from one of the threads, do whatever I need to do to notify it that that happened, and now the other thread can acquire the other lock and everything's good. This is not in general a very workable solution. Another way I can try to hack my way out of this is to disallow multiple independent requests. So imagine I just don't let you hold a lock if you're holding another lock. Again, I mean, this is something that will sort of by definition prevent these cycles from occurring because the cycles require that I need more than one thing, but this makes it potentially very hard to program in certain cases because it's natural to have a lock that protects a certain resource and a lock that protects another resource. I could create another lock that I get if I need both resources at once or something like that, but that's, again, just making things very hard for programmers to get right. This is usually the best exit out of this situation is to exploit the fact that I need a circular dependency graph for this to work. So if I can establish some sort of ordering on my locks, then you can convince yourself that this can never happen. So going back to this example, well, I won't go backwards. Going back to the example of the two threads that we're both trying to acquire the same two locks, part of what created the problem is they tried to acquire them in the opposite order. If they had both tried to acquire them in the same order, there'd be no problem. And then I would encourage you to go back to that example and convince yourself that if I acquire, it doesn't matter what the order is. If I acquire them in the same order, I cannot get one. Okay, so I'm just gonna go through this really fast. I feel like some lightning bolt would come down and strike me if I don't discuss the dining philosophers problem when you talk about deadlock. It just seems like it's, I don't know, like passed on and like the laws that they give you. It's like, you can pretty much do anything as a professor, except not talk about this problem when you talk about locks, okay? So I'm not gonna get in trouble. I'm just gonna do it quickly. Okay, so I've got these dining philosophers. I have no idea who these philosophers are, but I do know that they're all philosophers. Maybe you know who some of them are. I think one of them is Discard and another one's Newton or something, whatever. I mean, none of them invented computers. Okay, so let's imagine that these guys are sitting around a table for some reason and being geniuses, there are five of them and they've brought five chopsticks that they are trying to use to eat a meal. Obviously five chopsticks is not enough for five philosophers, but they didn't think about that. They're philosophers, not engineers, so. And let's say this is how they acquire them, right? So this guy takes that one. Everybody starts by taking the one on their left. And then what you can see is that I'm stuck. I had this, this is another, you know, just a longer example of a cycle where because I have now, no matter who tries to get the next chopstick, I'm stuck. So all of them need a chopstick that is held by one of the other ones. And if you look at, you know, we go back to breaking this, right? So if I don't wait, for example, if I can't grab the second chopstick, I put down my first chopstick. I don't sleep waiting for that resource. I say, okay, can't get it and drop it. So if any one of these guys drops his chopstick, it's fine, then the whole cycle will break. But this is usually a better way to do this, right? So let's say I break the cycle here. So the problem is, and again, you can certainly convince yourself if you order these around the table, if you number them and everybody grabs the lowest numbered chopstick, then four out of five will grab the chopstick on their left or their right, depending on how you order them. One out of five will go the opposite direction because he's the guy who's got zero on one side and four on the other. And that philosopher is the one that allows the cycle to not fall. So if you establish an ordering, you're good. Okay. So Denlock is, just one more note before we go to the final example for today. So Denlock is different, but similar to another case that can result that you can cause using synchronization for something called starvation. So in Denlock, everybody's sleeping, nobody makes progress. In starvation, there are threads that are making progress, but there's another category of threads that are not making progress. And starvation is important because it allows one category of threads to prevent another category of threads from making any progress. This is something you guys will have to think about when you implement your reader-writer locks because it is very easy for naive reader-writer lock implementations to starve writers. What does that mean? It means that if I have enough readers that are accessing that resource, those writers will never make progress, ever. And that is a problem. That's clearly not desirable. Okay, so here's a trivia question. What do you guys think would be better? Denlock? I mean, obviously neither is better, but if you had to pick one, Denlock or a race condition? Who picks a race condition? Okay, interesting, who picks Denlock? Why a race condition? I like that, something keeps happening, that's good. Why Denlock? Because it's super easy to detect, right? Race conditions, so again, go back to the bank example. That bank example, Denlocks, you're gonna notice pretty quickly because someone is gonna say, hey, my deposit thing just stalled, right? Like the whole system will deadlock. If there's a race condition, you're gonna have customers being like, my account's $20 short and I don't really understand what's going on and of course you're like, these people are so dumb, they probably brought something and forgot. Anyway, so race conditions produce unpredictable behavior, that's very difficult to debug. A deadlock produces predictable behavior, which is everything stops and that is awesome. The nice thing about deadlocks too is the system tends to freeze in the exact state that you want it to be in, which is the state that's the problem, okay? All right. Okay, so I don't think I'm gonna be able to get through this today, but let me at least set it up and then on Wednesday we'll come back to it and then we'll go on, okay? So this is kind of what we described before. Two types of threads share a fixed size buffer. Imagine I can have arbitrary numbers of either thread, I obviously need one, at least one of each kind. The rules are that if the buffer is full, producers have to wait, if the buffer is empty, consumers have to wait, otherwise everybody should be busy. So if there's an item in the buffer and there's a consumer that's available to consume it and do whatever processing we're pretending is happening, that should happen. And also if there's a slot in the buffer and there's a producer with an object that's supposed to go in there, that should also happen. So we wanna make sure that everything is kept busy, but also that there's no spinning on either side of the buffer. So I shouldn't be banging the buffer waiting for items or I shouldn't be banging the buffer waiting for space, okay? So these are the invariance that we just set up, right? So we wanna make sure that the producer waits if the buffer is full, the consumer waits if it's empty. And the producers and consumers should not be sleeping if they have work to do. And these are the things that are sort of tough to get right. Okay, so here's our two functions. These are kind of like the starter code. It's not a terrible idea when you're approaching problems like this to kind of try to write the code the way you think it should work in a serial setting. So if there was no multi-threading, this code works fine except for this. Actually, you can take this out, right? So if you remove these two conditions, this code will work fine in a serial setting, right? The producer puts an item in the buffer and increases this count. The count stores the number of items that are in the buffer and the consumer gets an item from the buffer and decrements the count, okay? All right, so on Wednesday, we'll come back and we'll finish this example. Please get working on assignment one. We have office hours all week. I'll see you guys on Wednesday.