 All righty, welcome back to 353. All right, hell week for you guys, I assume, with a bunch of exams, yeah? So, how many of you have done the lab four pre-lab thing? No one? Would make you feel better if it was due like Saturday? Yeah. All right, it's due Saturday, whatever. Even though it takes like 15 minutes, but still, whatever. You have three midterms this week, although, yikes. All right, so, we get to talk today about semaphores. Oh, and I guess I'll also release feedback forms and other stuff to look forward to. So, we get to learn about semaphores today. We should review locks, so locks ensure mutual exclusion. We can have a lock and an unlock called a critical section. Only one thread can be executing that code at a single time. Doesn't help you ensure ordering between threads, so how would we ensure some ordering between threads? Well, hopefully that's the topic of today. So, here is our problem. So say we have two threads. They both have a print line. One prints, this is first. The other says, I'm going second. And then, if we actually execute this, well, we should be able to reason about things that it could do. So, most of today will be fun code examples. So, here is that code. So, in the main, we create two threads. Tell one to run print first. Tell the other to run print second. And then, once we've created two threads, which one runs first? Don't know, all right. Everyone should be shrugging their shoulders. Don't know who runs first. So, after that, we join them. So, the main thread waits for them both to terminate, but we don't know which one is going to run first. So, if print first runs first, we get this is first. Print second runs first, and we see I'm going second first. So, if we go ahead and run this. So, I execute this. I see I'm going first, I'm going second. Does this mean I am a programming God and this happens every single time? Yes. Yes, no bugs. And we just send it, goes off to production. And what happened yesterday? All meta shut down. So, we just killed meta, yay. Probably not a bad thing. All right, oh, I executed again. Oh, now it's wrong. Okay, now I'm the worst. So, do we know how to solve this with, can we solve this with mutexes? So, let's think. Is there a way to ensure ordering between threads using mutexes in a way that is actually not undefined behavior? So, I will argue maybe watch me be a programming God. So, I will create a mutex here. I'll call it mutex and then I'll just initialize it. Say I always want to run this is first. So, what I'm going to do is I will put a pthread mutex lock right here. So, it's called mutex. So now, if I put a lock right there, well, that would acquire the lock and then this doesn't help me yet. So, I'm not quite there. So, I want to definitely prevent this line from going first. So, maybe I just like try locking again. So, I lock it and then I have the lock and then I will try to acquire it again, which would prevent me from running, I'm going second, but right now that would just prevent me from ever running, right? If I run this, it should freeze. Oops, that's the other course. So, it should just freeze and all I'll see is this is first because the other one's just waiting over and over again. All right, so here's where I become a God. I do this. So, I just have this thread unlock the mutex after it prints. So, now if print first executes first, well, let's say print second executes first. So, it locks the mutex. Okay, so it has acquired the mutex. The value has changed from a like zero to a one and then we lock it again. So, it's waiting for something to unlock it and then I execute the first thread, prints off this as first and then I unlock the mutex. So, now it can relock it and then print I'm going second. Does this work? Am I a God? Well, we can execute in C. Is there any problems with this crap I have wrote? Yeah. Yeah, so I never unlock it again. I could do that. I could just do this. Yeah, yeah. So, I have one bad situation here. So, remember, these two things run concurrently so we don't know when they can switch back and forth. What could happen, which isn't great, is print first executes first, prints off this as first and then unlocks, which the mutex currently is unlocked. So, unlocking and unlock mutex is undefined behavior. So, we have just screwed ourselves there and then also if we did that, then the second thread would lock, let's just assume that unlocking and unlocked mutex just did nothing. Well, then the second thread's going to just lock, lock and then it's going to be stuck there forever. So, if we run this, you'll see that it actually works. Most of the time, except when it doesn't. And when it doesn't, it's because the second thread here is stuck on this lock and the first thread already called unlock, so nothing is ever going to unlock it. It cannot make any progress. So, questions about that? Close, kind of a clever idea, right? I was kind of close. So, we'll see how to fix this. Oh, and also, what we need to argue about too is, well, we're kind of assuming printf is also atomic here. So, we assume that it just doesn't print everything all at once and that it, like we don't get something weird because an implementation of like printf, it could do like three system calls, right? It could do like this is first, all with different system calls and then the other printf could be also three write system calls. And if I go ahead and I run this, okay, sometimes it works, this is first, this is second, but sometimes when I execute it, there, I got something weird. I said, this, I'm, is, going, first, second. Could we fix this? Yeah, we could probably just fix this with a mutex, right? If I just, here, I just won't write the whole thing. If I just create a mutex here and then I lock, unlock and then do the same thing for the print statement, then that would fix it. So, that way I get a whole line at a time. I don't, I can't get switched halfway between it because if it has a lock and you switch back to the other one, it couldn't acquire the lock and we get stuck. Yeah, so, like why don't I have it around the regular printf statement? So, the reason I don't have it around the regular printf statement is it turns out that printf is thread safe, so it kind of behaves atomically. So, now whenever you use any function and you have multiple threads, you get to look it up and see if it's thread safe or not. Okay, that's the wrong printf. So, now you have to look at your documentation and at the very end of it, wow, it explains printf, a lot of printf, there we go. There's this attributes table and then there's one attribute that says thread safety and then if its value is mt, so multiple thread safe, then it goes ahead, it locks itself properly, it won't have any of the issues where we see with this one where we might see some letters interposed with the other one, so we don't need locks for it because they essentially guaranteed us that they have done it already. So, you get to look at a whole bunch of documentation now whenever you start implementing stuff with threads because any function you call, you have to be like, oh, can I call this with multiple threads safely? Yes or no? And most of the time it's in the documentation. Some, and if you're using someone's library and they don't say whether or not it's safe to use with threads, assume it is not safe and they don't know how to program it, especially if they don't know what the hell thread safety even means. So, back here. So, do you still see mist up if you write? Yeah, so in this case, if I went back to this and I did all in one system call, the system call will kind of behave atomically, so system calls are thread safe, thankfully, so we'd see the correct thing. Yeah, yeah. So thread safe in like the printf context means it won't, like if you have multiple threads calling printf, they won't interfere with each other. But if you give printf like arguments that might have a data race in your code, that's your fault. So, yeah, if I tell it to print a value that might be subject to a data race, your fault. It only guarantees that the printf itself is gonna all happen or all not happen. All right, so we want to fix this issue. So, we can't use mutexes. Oh, yeah, and another thing. Whoops, so if I go back here. So there's actually two things of undefined behavior here. So locking and already lock lock, well, that's technically undefined and you'll lock forever. And also, if it was successful and this one say locked twice and waited, well, remember what I said before, unlocking a mutex that you did not lock is also technically undefined behavior. So when it worked, it worked by chance. Technically, if we referenced the sea gods and all their rules shouldn't have worked, wasn't guaranteed to work, it just happened to work. All right, so center fours are the thing used for signaling. You might have heard this term before, probably not, has to do with like ship signaling and they have a bunch of weird flags and stuff like that. Thankfully, this one is a lot less complicated than that because it's basically just a number. So center fours, the way to think about them is that they're just like an int, they just have some value and it's shared between threads. Optionally, you can use it to share between processes as well, but just think of it as an unsigned integer or if you don't want to use the word unsigned, think of it as an integer that is always greater than or equal to zero. And we have two fundal operations we can do with it, a wait and a post and they operate pretty simply. So the wait, all it will do is decrement that value atomically and a post will increment that value atomically. So the only thing that makes this useful is this following rule. So wait doesn't actually return until the value is greater than zero. So if the current value of the center four is zero and you call wait, it will sit there and actually block and wait until another thread increments it from a zero to a one and then it can go ahead and decrement it from a one to a zero. So that number is never allowed to be negative. If you call wait and it's currently zero, it will sit there and wait for another thread to increment it. So initially we can set the initial value of the center four to whatever we want. And basically you can think of that as that's the number of wait calls that can happen without any post calls. So the API, kind of similar to pthread locks except it's in its own header file. So there's a center four.h. Do not use this on macOS. They decided they didn't like this and it essentially does nothing. So that's fun. So just like pthread mutexes, we can initialize it, takes three arguments. So a pointer to the center four structure itself. There's this optional parameter pshared. So that's whether or not it should be shared whenever we fork between processes. So if we just set to zero, it is independent for the process and then not shared, but shared between threads. And then that value is just the initial value of the center four. When we're done with it, of course we should destroy it. And then there's the wait. So that will decrement that value atomically and only wait if the current value is zero for another thread to increment it. There's also a try wait if you don't want to block. So you can just try to decrement it and it will tell you yes or no whether you successfully decrement it. Then there is post and post just increments that value. So like the pthread library, all of them return zero on success. And let's just not worry about this pshared argument. If you want to use it for inter-process communication, you can for this course, we'll just argue about center fours just within a process with threads. All right, so with the center four, we should be able to fix this problem. So the way to think about doing center fours is you have to do three things. You have to place like a wait, you have to place a post probably and you have to create it with some initial value. So easiest thing I like to do is the first thing you should do while it's probably the fine one. So we can just say, let's just name it SEM and I need to include the library. Hold on, let's go to center four dot eight. All right, so first thing I need to do is create the center four and after that, the first thing I like to think about is where do I need to place my weight, right? What do I need to prevent from running? So in this case, I want to prevent this function from running and well, if I want to prevent this print line from running first, I should probably place my weight like right before. So I should probably do a weight here. So if I do a weight here, well, I haven't initialized it, so I should next think about what the initial value of the center four should be. So I can call SEM and knit, give it the address of the center four. I don't care about p shared and then I need to think about this initial value. So if I set the initial value to one, will that do me any good? Probably not. So if I set the initial value to one, well, let's say either thread could run first. If print second actually gets picked to run first, we hit SEM weight. If the current value is one, it can decrement it from a one to zero and it won't do anything to prevent that from going first. So it's fairly useless like that. I can't make it negative. So if I want it to actually wait there, then I should set the initial value to zero. So now if thread or print second actually executes first, well, then the current value zero, it tries to decrement it, it's not allowed to, it has to block until another thread posts. So now I can think about, okay, well, when should I let the second thread actually make some progress? And where should I place my post? Well, if I place it here, will that likely work? Probably not, because what could happen is, okay, maybe this thread gets scheduled to go first, so it waits, so the current value is zero, it waits, it blocks, and then we have no choice, eventually we have to schedule print first, then it would increment that value from a zero to a one, and now we could switch back to print second because it could now be unblocked because now it could decrement it from a one to a zero, and then it could do this print f first, so we should probably just move it down one line so that the only way that this second thread is going to run this line is after the first thread is done with its print f function, and then it increases the value from a zero to a one, and then print second can go ahead and unblock it and actually work. So now if I run this, that was a called order print, I get, this is first all the time, and then I'm going second, and it always works. So questions about that signaling too? Where'd you get the word signal? Oh, so signaling in like the English word, signaling, yeah, so if you wait on it and you're blocked, it kind of waits for another thread to do a post, and then it'll be like, it'll wake you up and you can actually successfully get by the weight and actually do something. So that's what I mean by signal on that slide, like pretty much just unblocking it because you posted it so you're kind of poking other things that, hey, you can run now, I'm done whatever I need to do. All right, other questions for this? Yeah, so on macOS, you can't use this because the cypher board doesn't have a name, macOS forces you to name them, so it just gets more annoying. And another question, why can we do mutexes? We started off trying to do mutexes, but we couldn't. So might have to review that. In this silly example, oh, yep. Oh, what about if we have three threads? Okay, that's a good one. What was your question gonna be? Yep, you can do multiple posts in a row, multiple weights in a row. Yeah, you could just post over and over again. I could post 10 times in a row. It just atomically increments. Yep, or I could set the initial value to 10 or do whatever. So yeah, we have this question. So if we have something that wants to print, all right, let's make it excited. So I'm third. So that's a good question. What do I call it, print thern? I probably should have put that in a loop or whatever. All right, so yeah, let's think about this. Oops, non void function. Sorry, return null. So yeah, now if I run this, it should always print, I'm going first, I'm going second, but we have no idea where the hell I'm going third is gonna be, right? Could be first, could be second, could be third. So let's make sure. So second, it seems to like going second or sometimes third. Doesn't seem to like going first that much. So how would I fix this? Yeah, yeah, so in this case, I can't really reuse that same center for because let's say I just did this. So now I have two threads waiting. So now I have two threads waiting and in this case, while the initial value is zero, so let's say either thread executes here and this is kind of like what an exam question would be. So now either thread could run first, current value is zero. So both threads get blocked. The only thread that could run is the first thread. This is first would definitely print first and then it posts. So now it changes that value from a zero to a one and well now both of them could make it by weight but only one can make it by weight this time, right? So we'll either see I'm going second, I'm going third and then nothing, right? We have no idea. So if I go ahead and run that, let's see that I'm right. So now I saw, oh, the first one and the second one and then the other one got stuck and then oh, now I got good luck. This one, the third one run, not the other one. So this, if I wanted to not have any order, I could do what you suggest and just do two posts. So if I do two posts here, I won't have that problem where one gets blocked forever. So they're both waiting and then once print first is done, it increments it twice. So zero to a one and then a one to a two or it could do it in a slightly different order. Let's just say it was two. Well, if it's two, both of these can pass weight. Again, we don't know what order they're going to be in but at least both of them are going to print now. So still didn't quite solve a problem. We got some of the way there where this is first always goes first. But like you said, we probably just need another center for. So to fix this, we just create another center for, I'll name it SEMA2 because I'm creative and we could just go ahead, initialize it with the value zero and kind of do the same thing again. So this was our solution for a single like this is first, always printing first before this is second. So we can do the same idea before. So this is third has to wait for this is second to print. So it could wait on the center for its initial value zero. So it can't make any progress. And then we could post it only after we print this only after we print I'm going second like this. So now I have two center for's now when I run it, it's always going to be an order all every single time. Yeah, yeah. Yeah, there's not really any smarter thing to do than this. You could write it in a loop or something like that to help you out, but yeah. So if you wait on the first and post, wait on the first and then wait on the second and then post the first, well then essentially if you only use two center for's and say you have a hundred things, you've like 50 of them waiting on like waiting on the first center for 50 of them waiting on the second and then they post the other one. So when you post it, you don't know which of the 50 is going to run. So you would need 99 otherwise you've no idea. So yeah, a lot of people, so don't get into the trap of like trying to reuse center for's cause probably it will be wrong. Even if it seems to work some of the times and yeah, another question about doing try weight instead of weight, I'd have to add like air handling code to it. So like I could maybe it's like a performance sensitive application. I could do try weight and then if I didn't decrement it means I shouldn't print third, but I might do something else that's like independent for the thread, things like that. And yeah, so that works for that. We'll see a more complicated example at the end. So here's the solution for you. So one, just make a center for weight in the second thread post in the other one and now they work all every single time. So you might notice that set of four is kind of maybe look like a special case of mutexes. So turns out that we could actually use the center for as a mutex. So how would we do that? They kind of look the same, right? Yeah, yeah, close. Yeah, so you set the initial value to one and then instead of lock, you do weight. So only one thread could make it go from a zero or a one to a zero. If another thread tries to weight, same thing as locking only ensures mutual exclusion. And then instead of unlock, you post. And as long as you follow that, so the only difference is mutexes would have to like the lock and then the unlock would have to come for the same thread for set of fours, you don't have that restriction. But if you were careful and you always made sure that instead of lock, I did weight and instead of unlock, I did post and I set the initial value to one, then it behave exactly like a mutex. So if you want mutual exclusion, even though you could use the center for mutexes are a bit easier to understand because you don't have to set an initial value but if you really wanted to, you could. So you could do like that count example here where I set that center for the initial value to one and then instead of lock, I do weight instead of unlock, I do post. I would suggest against that because it's very easy to screw up. So it being correct depends on this initial value which is typically far away from the code. You might not even be able to find it if it's a big code base. If someone comes along and changes this one to a zero, what happens, you're screwed. So whenever it calls weight, it'll just block there forever. If someone comes along here and changes the one to two, you're also screwed. You don't have mutual exclusion anymore. So point of the story is if you want mutual exclusion just using mutex, it's a lot harder to screw up than using a center for for mutual exclusion even though you could. Good rule of thumb is if I wanna sure a border between threads, use a center for. If I wanna ensure mutual exclusion within a thread, use a mutex. So now let's make our problem way harder. So can we come up with a solution to this problem? So assume you have a circular buffer. What's a circular buffer? Just a fancy word for just an array that if you make it to the end and you go to the next element, it just circles back on itself and restarts and goes back to the first element. So we will assume we have a circular buffer and we have a bunch of consumer threads that are going to read the data from it and do something and then producer threads which are writing data to this. So the rules are the producer thread should just write data to the buffer only if it's not full. So let's say we have a lot of producer threads. Well, if it fills up the entire buffer with values, we don't want it to overwrite values that a consumer has not read yet. And similarly, while the consumer should only read data from the buffer if it's not empty. So if it has no data in it initially, the consumer shouldn't just read some random data. It would be useless and probably be wrong. So the rules for this to make it a bit easier too is all the consumer threads share an index and also all the producer threads share an index. And I went ahead and made sure they were thread safe so that when we have multiple producer threads, they would fill up in order and they wouldn't interfere with each other. And initially they both start at zero and they increase sequentially. Yep, yep, so we already skipped ahead to the solution. So let's look at the code and we will develop the solution. So here we go. So this is a lot of code. So this first while thing, I probably should have just hid this from you. So internally it's using center force to just figure out how many values it needs to produce and consume. So you can just read this while, like while there's still something to produce because I'm assuming I have a finite amount of something. In reality, you might just have this running forever. So in order, because this is the producer code, I will just sleep for a little bit of time, simulate doing some work that I'm assuming is independent per thread so they can all run in parallel. And then I will fill a slot. So that is going to be all thread safe. So it's just going to start filling slots, starting at slot zero. It's just gonna be an array up to, I believe in this case, my array holds 10 elements. So start just filling them up in order. Now the producer or the consumer, same thing. So it is just going to consume some data. It doesn't have to wait because, or it doesn't have any work to do until it gets some data. So first thing it does is empty a slot and then it simulates doing some work on that value. It could be doing anything that is fun. So if I go ahead and run this just as is, I don't have any synchronization between them whatsoever. And let me see what the default values are. So I can say the number of producer threads, the number of consumer threads, and then optionally the number of elements to produce, the size of the buffer, and then how long each of them takes. So I'll just say, let's say we have, I don't know, four producers and four consumers. So if I run this, chaos assumes. So if I go back at the beginning, I essentially print a red message anytime something bad happens. Now there's two bad things. There is emptying a slot. So the rule should be that you do not empty a slot that has not been filled up yet. So first thing that happens is one of the consumer threads tries to empty slot zero. It doesn't have any data in it yet. So it complains at me and turns out it looks like all four of my consumer threads run before any of my producers and they all just read invalid data, which isn't good. Then we go through my producer threads, fill up those slots, but the consumer threads are still just chugging away and they're emptying more slots. Then we fill some more slots and then it empties some more. And then finally it loops back from slot nine all the way to slot zero because it's a circular buffer. There's only 10 elements. So this actually read valid data. This actually read valid data. And now we are filling slots eight, nine, zero and one. So that's okay because it just emptied out this slot so we can fill it again. And then it empties out some more slots. We fill some slots and we are good. In the case where we have, let's see, more producer threads and consumer, well, we have more producers. So eventually we get to the situation where we essentially lap the consumer threads and we start filling slots that are already filled. Yeah. So fill slot will write a value to that buffer and then empty slot will read a value from that buffer and since we read it, it should be able to be reused for something else. So shouldn't we all? Yeah, so we have a few things we need to solve here. So first, like I said, probably easier to think about like placing weights and giving center fours names. So there were two problems with this. One was emptying a slot that isn't filled yet. And the other was filling a slot that is not empty. So maybe I make a center four, let's say I call it. So some try weight just goes ahead and tries to decrement it. So this is just a silly hack to make sure that between all of my producer threads, it produces 15 values and all my consumer threads consume 13 values or 15. So don't wait about that. That's just some, yeah. Basically this just takes advantage that this will just atomically decrement it and it's atomic. So the body of this will only run 15 times between all of my producer threads. And then once it's zero, well, it's not successful and then it just finishes all my producer threads. So just a little hack so that between all my producer threads, they run 15 times and all my consumer threads run 15 times. All right, so in order to fix the first problem, I might create a center four called number of filled slots. So with that, first thing I want to think about is when I should wait. So I should probably wait right here before the empty slot. So I could do weight on a filled slot. So this would make sure that none of my consumer threads would run hopefully what I want before my producer threads run. So I want to prevent them from running immediately so they don't read any invalid data. So after I place that weight, I should probably think about what is the initial value of that center four I want. So let's say, what was it called, filled slots? So what should that initial value be? Sorry? Zero, right? There's initially no filled slots. Yeah, so yeah, initially everything's empty. So if that initial value is zero, well, if I place a weight here, that makes sure that no consumer threads will run before any producer threads run, right? So now I can think about the post. Well, if I fill a slot, I have filled exactly one slot. So I should probably just post that value. So if I post it, whoops, that will let exactly one of the consumer threads make it by weight, right? So if one producer thread fills up one slot, well, we post once. So now one of my consumer threads can run, empty the slot that just got filled up. And let's say all four producers ran first, well, we would post four times. So the value that center four would be four. And now exactly four consumers could make it past this weight. So we could decrement it from four to three, three to two, two to one, and then one to zero. And then all the other ones have to wait. So if we run that, hi, we fixed it, right? No red, all good. Am I done? Yeah. Yeah. Yeah, so this one happens to work because in this case I have more producer threads and consumer threads, so they kind of lag behind. But if I made two producer threads and four consumers, likely when I run it, okay, we're not gonna get unlucky. Okay, it still seems to work. Still seems to work, god damn it. Oh, okay, that one's, there's way too many. Oh, more consumers. Oh, more producers, all right. There we go. Okay, didn't think that through. Yeah, so if we have more producers and we have more consumers, the consumers or the producers start lapping the consumers and then we get into the situation where we are writing or we are trying to empty a slot that is already empty. So we kind of lag behind. So in order to solve the other issue, we want to essentially have another set of four that keeps track of the number of emptied slots, right? So if we keep track of the number of empty slots, again, we should think about where we want to put the weight. We probably want to wait here, so we want to wait for the empty slots. So now we can think about what the initial value of emptied slots should be. Oops. So should the initial value of emptied slots be zero? No, what should it be? Yeah, it should be n where it ends the buffer size because right now, if we said that zero, well, producer threads should be able to run first, but then they would all hit weight and current value is zero, so they all block there. And then current value of the filled slots, therefore, is zero, so they all block, they all get weight there. So if we want to think about how many could run at once, well, they could all run in parallel, like up to 10 of them could all run in parallel and all fill up the buffer. So we want to be able to let 10 of them work. So it turns out that I defined the buffer size. Yeah, so there's a variable called buffer size. So we should set the initial value to the buffer size so that way we could have, in this case, all 10 go. So next thing we need to do is place the post and all we want to do is put the post here. So after we empty a slot, we just post up the number of emptied slots. And now, if we run this, okay, boom, no problem, they're all waiting, right? No way that they can't wait out of order or anything like that because everything is bound by their center force. So there's one center for keeping track of the number of filled slots, one keeping track of the number of empty slots and those two by themselves just enforce all of our rules. So any questions about that? Yep, so I just wrote that just because that demonstrated our problem too where there was like, we started emptying slots that were already empty, I think we're writing the slots that were already filled. Yeah, so that was just to demonstrate the other one. I could change them around. So let's say I have 10 of each, boom. So I could have 10 of each, I could have 10 or one producer, 10 consumers in which case, well, because I have that, they're essentially all bottlenecked on the one producer. So as soon as we fill a slot, one of the threads goes ahead and immediately empties that. So, but if I have five and five, they kind of alternate and you could see how this affects them because maybe the consumer takes less time than the producer and then the number of threads that would be optimal might change depending on your workload. And yeah, why do I have a weight here? Okay, you're not sure, I might have to do the discord comment after, but so this will always work, right? Any arguments about that? And the nice thing about this is, well, this thing that's actually simulating doing real work, which is hopefully most of your program, this can be all done in parallel with all the other producer threads and all the other consumer threads, they don't have to be mutually exclusive because, well, we're assuming that they're independent and then if that was fast enough, this thing would use all of your cores, it would be super fast and it would be great. Yeah. Yep. Yeah, so, so depends on how we implement weight. So it might be like mutex where it might keep like an order if who called weight in what order and then unblock them in that same order if your question's fairness. Yeah, so the implementation probably will do something with fairness. So that, yeah, I don't have to worry about it. And in terms of using this, like even if it wasn't unfair, it would still work. It might not work as good as you would like. And in that case, you just blame whoever wrote that and use someone else's implementation, that's better. But with all things in computing, oh, maybe that's like way slower and then maybe you wanna use something that's unfair and it seems to work out for you. Again, who knows. All right, any other questions with this? Yeah, yeah. So to do this in practice, like this fill slot has to be thread safe. So it writes data to it in a way that another thread won't try and read what we're currently writing to it. So if you look at, so you have all this, I made it thread safe because I did mutex locks. You don't have to actually read this stuff, but it's all there, so it is all thread safe. Yeah, so there's one mutex that protects fill slot and empty slot because they're both modifying the buffer. So there's a potential data race there, right? So I have to have a lock. Safest thing is just to put it around the whole thing. That way I don't have any issues. So the idea is fill slot and empty slot cannot run in parallel, but that is like reading and writing a value to a buffer doesn't take very long and you would hope that this dominates. So this, that sleep can be done in parallel, assuming that they're all independent. But yeah, if you wrote this and then the amount of work you have to do in each thread takes less time than it does to actually get data from the buffer, then probably you just don't wanna use threads and it's just gonna make things slower. But always the thing in the follow-up course for that, you'll learn tools where you can go ahead, profile it and see if it's worth it or not. All right, so solutions are on the slides. So yeah, so we use center fours and share proper order, have an initial value we can choose, increment it using posts, decrement it using weight. They're both atomic, weight blocks at the current value is zero. Still need to prevent data races. This should only be for ordering. So just remember, phone for you, all in this together.