 A lot to do today. All right, so today we are going to finish talking about fork. And then we're going to change gears for a couple of lectures. So normally what I've done in the past is I've continued working our way through the OS interface, particularly the process-related system calls this year because of the assignment one deadline being nice and early and getting everybody out of the gate working on the assignments, which is awesome, by the way. It's really exciting. But I want to cover synchronization in class before you guys do it on the assignment. And synchronization is sort of the wandering pericope of topics in OS. It can sort of go anywhere. So I've engineered a little transition from the end of fork into synchronization. We'll do a few lectures on synchronization. And then we'll pick up talking about the system call interface and the process lifecycle. I guess maybe I'll show some of those lectures next week while I'm away. OK, just a couple of announcements. Please, if some of you guys have started to work on assignment one, that's good. Everybody should be working on assignment one. If you're not working on assignment one, you're behind. You have a week plus two days to complete the assignment. That's doable, but it gets less doable as time goes on. Please work on the assignments with your partner. So if you've seen the collaboration guidelines, we actually asked. And I know that some of you guys have had bad experiences with partner work in other classes. So in this class, I actually expect you guys to work with your partner. Some of the type of things that might have happened in some of your previous classes were, for example, you did all the work. How many people have been in a partnership where they felt like they did all the work? Now here's the problem. I just want to point out something about human nature. Most of us have a tendency to overestimate how much work we actually do on any given project. So I suspect that if you ask a lot of groups how much work they did, the answer adds up to quite a bit more than 100%. And that's, in general, a good thing. It's good to feel good. I mean, why not? Who cares? It's a free 50%. This guy did 70%. That guy did 80%. They both feel good about themselves, and the project got done. But anyway, how many people were in a group where they felt like somebody else did all the work? Great, you guys are being honest. Probably everybody's hand should have gone up. So in this class, that is actually against the collaboration policy. We don't necessarily have great ways of finding out when that's happening, but if we do find out that that's happening, then I consider that to be an academic integrity evaluation on the order of cheating. You guys are not allowed to do the entire assignment or have the entire assignment done for you. That's actually against the collaboration policy. Now, I'm not expecting everyone's going to get to 50-50, but you guys need to work on the assignments together. All of them. That also doesn't mean I'm going to do assignment zero. They're going to do assignment one. We're going to trade off back and forth. You guys need to be working on the assignments in pairs. Now, one of the best ways to do this, I think, and I'm going to suggest this this year for the first time explicitly, is to actually program together. Sit down, find a couple hours a week, sit down at the computer. You don't have to both try to type, but both of you should be looking at the screen and you guys can actually try to write some code together. I think particularly for the systems assignments that you're doing in this course, this is a great idea, because you may think it's faster to split things up. Every problem that you guys create in your code is going to take a lot longer to solve than you're used to. If you have somebody sitting right next to you who's saying, I don't know why you did that, or you never initialized that variable, or that pointer has no value and now you're dereferencing it, that will be really, really helpful. It'll save you a lot of hours later. It may feel like you're writing the code more slowly than you're used to, but if you cut your debugging time down by, I don't know, 50%. You will be happy. You will be out there in the spring chilling out, enjoying the sunshine whenever that happens, maybe June, having a brew ski, everybody else be working on assignment three and be like, we're done, because you're gonna find out. I mean, 80% of the time that you spend on these assignments is probably debugging. So if you can reduce that time, even if it takes you twice as long to write the code, it's a good deal. So think about it, and this is a nice way to make sure that you're working together, too. Okay, so last time we started talking about fork, started talking about where processes come from. Many questions about fork. All right, let me just do a little bit of review. So remember, we had to update our process model to introduce a level of indirection, right? A level of direction was designed to allow processes to share file handles after fork, right? And so we introduced a file table that's private teach process. File descriptors that are used by read and write are indexes into this file table. The file handles are initially able to be shared immediately after fork, right? And so this was our new way of mapping to files, right? File descriptor maps to the file table to a file handle and the file handle actually maps down to the internal file objects that the kernel maintains, right? The reason we did this was because we wanted to allow file handles to be shared after fork and I will show you exactly why we did that today, right? When we start talking about how we can use pipes to set up chains of communication between cooperating processes, okay? And the file objects are something you guys don't really need to worry yourselves about here, right? Okay, so remember what fork does, right? Fork is designed to make an identical copy of the calling process with one exception and only copies one thread in cases where the operating system supports multiple threads, right? I copy the thread that called fork, I copy the entire address space and I copy the process file table, right? That results in both the parent, the process that called fork and the child, the process that is being created, sharing essentially what are pointers to file handles that were opened by the parent when it called fork, okay? So I copy the thread, I copy the address space and it was a result of copying the file table, the child has these very horizontal lines that point at the file handles that the parent had open. Questions about fork, everybody remembers this. So I will, I'm not gonna mention this more than once and I will do my best to ignore it but if you're in one of the seats that makes that noise and you know that it's happening, please move to another seat, it's super irritating, right? And I'm sure I'm not the only person who's in it but I know that every seat probably does it in some way, does every seat do it? Can you guys try all doing it at once? All right, so at least now I know that's the worst it can get, okay? But anyway, yeah, if you can avoid making that noise I'd be very happy, okay? And I think people watching these videos later, when you're showing these videos to your children later, they won't like that noise either, okay? All right, so remember fork is weird, right? It's probably one of the weirder functions you've ever used because it returns twice. It returns once to the parent process that called it and it returns a second time to the child and that's the only difference between the parent and child is the return value of a successful call to fork, yeah. Oh, of course, yeah. Everything, this is the beauty of the OS 161 environment. Everything you do is all simulated, right? There is no way for your code to do anything malicious to the underlying machine, right? That is why we, or I shouldn't say we, that is why David and Margot set up this environment in the first place, right? It's why we use a simulator. If you could make changes to the underlying machine, you could do bad things to the underlying machine, right? Now again, back before we had virtualization that was more of a problem, right? Now, maybe you don't mind rebooting your VM every few minutes, it's kind of a pain, right? It's faster to restart this 161 after your kernel panic and you will do that a few times. It's a good question though. All right, any other questions about fork? All right, okay, so you might wonder why have we, you know, what have we done this for, right? You know, it seems like we've complicated the way that we manage processes references to files and so far we haven't got anything out of it yet, but now we're going to, okay? So the pipe system call, I think we've brought up before, it's an IPC method that allows two processes to create something that looks like a file, but it's not a file. Instead, a pipe creates pretty much what it sounds like. It creates something that has two ends. You can put stuff in one end and when you write to one end of a pipe, the other end can be read from, right? The contents that are read and written to the pipe are buffered in memory, right? They are not stored on disk anywhere, right? How many people have ever used named pipes? Ooh, there we go. I get the real, real sort of UNIX hackers here. So you can actually, there's a way to create in your VM a thing that looks like a file. It sits there in a directory like a file and yet actually what it is is what's called a named pipe. There's no contents on disk corresponding to that file, but if one process opens the file and writes something to it and another process opens the file and reads from it, it allows them to communicate, right? Usually you guys use pipes implicitly. Again, all of you guys have used pipes because you've piped output from one command to another, right? But those pipes are set up more transparently. Named pipes are kind of a weird thing and usually only useful when you're trying to do something terrible that you really shouldn't be doing. Okay, I think you guys would agree, right? I mean, okay, so anything I write to one end of the pipe, immediately available at the other end, okay? And the pipe contents are buffered in memory, right? Why is this useful, right? So let's look at how we can create these nice chains of processes by using the two system calls we know about, pipe and fork, okay? So here's what happens. Before I call fork, I open, the parent opens a pipe. Then it calls fork. Now after, so when you open a pipe, you actually get two file descriptors from the kernel. Why? There's one for the read end of the pipe and there's one for the write end of the pipe. Now, every year someone asks this question and it's one of the things I forget in between giving this lecture. I think you can write and read to both ends of the pipe. I think there are bi-directional pipes where data can flow each way, but let's just pretend for now that pipes work one direction. There's a read end and a write. When you call pipe, you get two file descriptors. One you can write to, that's the input end. The other one you can read from, that's the output end. So when I initially call pipe, I have a circular buffer that I can play with myself. I don't know why you'd wanna do that, but you could potentially have one process that was writing and reading data to itself, essentially. Again, not sure why this is useful, but so what I do is after fork, the parent and the child play a little game. The parent closes one end of the pipe, the child closes the other end of the pipe and now what they've created is a way for one process to write data that can be read by the other process. And again, because this is a file, it's nice because it looks just like standard input, for example, right? So if my second process is grep, grep is just doing its thing. It's just reading from where it would normally read from. It doesn't know if it's standard input or from a file and it's looking for things and printing results. So here we go, with a diagram, because I know that's hard to visualize. So here's what I do, I'm the parent. I create my pipe. Now, when I initially create it, I've got the right end and the read end. I don't know why this is a triangle. It's supposed to be an error going the other direction. Now, when I call fork, here's what the child looks like. Now, if the child has other file handles open, those will be shared too. But remember, after I call fork, I'm sharing both file handles. So now both parent and child have two file handles open that put to the same pipe. Now, the parent closes one end, the child closes the other, and voila. We have IPC. We have achieved IPC. Really two lectures worth of weirdness about how file handles are used and this is the result and this is why. So here it is again. Create the pipe, two ends, call fork, four ends, close the ends I don't need, two ends. And I can write data into the child, which can read it. Here's how this looks. I'm not sure this would actually compile, but it's pretty close. This is my little array of file descriptors or in this case just ints. That's going to receive one file descriptor for the right end, one for the read end. I call pipe. I pass in this little array. The pipe call initializes that array with the file descriptors that are returned. Now I call fork. This is terrible, I should check for errors, but I'm in class and this is never going to run, so I'm okay. If return code is zero, who am I? If the return code is zero, who am I? I'm the child, right? So the child is going to close one end of the pipe. Let's pretend pipe ends one is the... So if the child is closing pipe ends one, what end is pipe ends one? It's the right end, right? We're pretending here that the parent is writing data to the child, right? So I don't need the right end. And then I can start reading data from the read end and of course I would do this in a loop or something. I wouldn't do it like this. Now down here, who am I if return code was not zero? And what is return code for the parent? Process ID of the child. So I close the other end and now I can write some data into the pipe, right? Any questions about this example? Yeah, yeah, exactly. Sorry, did I say the right end? I'm in the read end, right? Yeah, yeah. So if you look here, the child closes one end, the parent closes the other end, right? So regardless of which end it is, the child and parent close different ends so that they have, you know. So I could set this up so that the child was writing to the parent, right? But in the example I've chosen, pipe end zero is the read end, yeah. So I didn't need to call that the right end, that was my bad. Good question, any other questions about this? Yeah, is there some sort of synchronization required? I don't think so, and I think the answer is no. I think what will happen is if the child executes this first and gets the recall before the parent has run, the recall will just block until the parent writes, right? So yeah, a read on a pipe will block until there's data in the pipe, right? Or the pipe is closed, right? So if you've ever had a case where you've tried to set up this long gnarly Unix shell pipeline with like six commands and one of them dies in the middle, usually what happens is when it exits, the pipe gets closed and the processes that are farther down the stream will exit because they're trying to read from a file descriptor that's not closed, right? So that'll cause them to exit. Good questions, any other questions about this? Okay, so let me, before we go on to synchronization, just let me talk about a couple of problems with fork that have been addressed by some nice engineering techniques. So the first problem is that fork ends up copying a lot of stuff. What do you think, so fork copies what? They're talking about three main things, what's the first thing? How much state do you think threads have? Small, medium, large. Medium to small, I'd say small, right? The file table, big, small, small. So what's all this state? Contents of memory, right? Remember, after a fork, the child and parent need to have their own separate address spaces. So all the content, anything that the parent had access to in memory, I have to make copies of, okay? So that's kind of too bad. Now, here's another question. When I call fork, right, what is the process that called fork usually trying to do? Is it actually trying to make a copy of itself? So if most of the processes that called fork made a copy of themselves, what would be running on your machine? Like you'd have a bunch of copies of init, right? Just not what's running on your machine, right? So normally when I call fork, and again, we're not gonna go on and talk about exec yet because we're gonna stop and do synchronization, but normally when I call fork, the next thing I'm about to do is reload this child with a completely new image, right? I wanna make a new process, it's not like me, right? For example, your shell, when you type a command into your shell and it executes it, that's what it does. The first thing it does is it calls fork and you end up with a brand new copy of bash. You don't want a brand new copy of bash, right? You want it to run PS. So the next thing that the child does is it calls another system call that completely wipes out the entire address space and starts over, okay? So I just copied all of this state for bash and now the immediate thing I do next is I tell the kernel, I don't wanna be bash anymore, right? I don't, no one wants me to be bash. You only need one bash at a time. So this is an issue, but this is the way that these systems were originally built, right? There's part, you know, if you look at the Windows API, for example, it's a lot more complex, right? But Unix has always just tried to keep things simple. So I don't have like a fork and exec command or I didn't initially, I called fork, made a copy myself, then I can make changes, all right? There's been several ways that we've solved this problem. So without creating a new system call, can you guys think of some approaches to solving this problem? Particularly the fact that I'm gonna copy all of this state and then potentially immediately get rid of it and go on to do something else. Any ideas? Brilliant systems engineers, yeah. Yeah, so here's the thing, I want your clothes. I want a solution in the kernel, right? I want applications that can continue to be able to do their own thing, right? And your shell and every other Unix application does this. Like this is the design pattern. So I don't wanna have to change the apps, right? But what could the kernel do? Yeah, okay, that's dangerous though. How do I figure out what I intend to share? How do I optimize for the common case? I just told you the common case is I fork and then I get rid of all the state, right? So what can I do? Yeah, yeah, so there's a very clever approach that we'll come back to when we talk about memory management that's called copy on write. And copy on write is actually applied most of the time that fork is used because there's two cases. So what copy on write does, what's your name? Junko, what Junko just told us is essentially what I'm gonna do is I'm gonna create pointers from the child address space to the parent's memory, right? Now, as long as the parent and child don't change that memory, everything's fine, right? When either the parent or child modify the contents of a particular chunk of memory, then I make a copy, right? So that they have their own separate copies. So this is called copy on write. We'll explain it more in a few weeks, right? The other way to do this is just change the semantic slightly, right? So there's a call called V fork, right? V fork will fail if the child does anything other than immediately call exec. So this is essentially a new system call that I can create. So it broke that first thing. I didn't want to have to change processes, but in cases when processes are going to immediately call exec, I do V fork and I just don't copy the address space. Or maybe I need to copy like a tiny bit of it just so the child can execute the next instruction, but I don't copy most, right? The other thing I can do is I might want a lot, so there's a new system call that's been created that sort of supersedes fork that gives processes an enormous amount more control over what gets copied, right? So if you do man, it's called clone. And clone has all sorts of options that essentially allow the parent to customize the child process, right? I can say I don't want my child to share the file table, but I do want it to share my signal handlers and I would also like this and that, right? So it's sort of like clone gives you a menu that you can order off of and allows you to design a new you, right? It only shares things that you actually want to be sure, right? And for example, you can also give clone and a different entry point, right? So cloned processes don't even have to start at the same point that the parent called fork, right? So this is, this is a lot in many ways, a lot more power, right? You guys can find out about this run man clone, okay? So someone asked before, you know, what happens on a running system after a period of time and there's a cool utility called PSTree that you can run that will show you essentially the tree, right? Because that's what's happening. Every time I call fork, a parent is creating a new child, right? And you can visualize that as a graph where calling fork creates a new child node, right? And here's an example of the output of PSTree. Unfortunately, of course, this is my command line and so it's hard to show a graph, but here's how they did it. What's the root? What's the root of the tree? Init, right? So I think someone asked about this on Piazza. The operating system sets up the first process by hand, right? There's no one to call fork, it's the ultimate chicken and the egg problem and so the OS creates the egg, right? Which can then call fork and create other things, right? So you'll see that init in particular has created a bunch of different childs. How many people have ever sort of configured the init scripts of a machine that you administer before? You know, like to turn on a web server or set up a mail client or run your bit torrent tracker or whatever the heck it is you're doing. You'll see that most of these type of processes are system services, right? So cron, which is something that runs things periodically, XM4, anyone know what XM4 is? Linux system administrator quiz. XM4, it's a mail client. Git daemon, it's the tool that allows you to clone the OS 161 sources before you're logged in. SSHD, oh, come on, this is easy. SSHD, server allows you to SSH into the machine so I can run these commands. So these are all sort of system services, right? Now you'll see that SSHD has created several copies of itself for some reason, I'm not exactly sure why, but SSHD has several children that are also SSHD. It's probably called fork several times so that it can handle a couple of different connections at once, right? And finally, here's bash and here's ps tree, right? So this is the shell that I'm running and this is the command that actually generated the tree, right? Up here you can see that I have a couple of web servers running as well, right? So you see that Apache has actually forked itself and then that copy of Apache has forked a bunch more times. So I'm remembering this now. This syntax right here indicates that there's a bunch of threads that are running, right? So this is a case where it called clone and created a thread rather than an entire process, right? So this is what Apache does, right? Apache wants to have a bunch of different processes and threads ready to handle incoming connections. Yeah, yeah, yeah. Yeah, Apache now, by default, I think uses a mix of threads and processes and I can explain why that is, but we can talk about that later. Performance reasons, I believe. Any other questions about this? Okay, so now, let's fast forward, again, normally about a week and a half in the course and let's start talking about synchronization, which is something that you guys are gonna counter on assignment one, okay? So what you need to know, because normally we'd be downstream of here and there's a few things that we would cover, but it turns out there's not a lot that you need to know to understand why we need to synchronize things and to start talking about how to synchronize things, okay? The first thing is that operating systems create an illusion of concurrency. How many people think that there are multiple things that are going on on your computer at the same time? Normally, right? I hope so, right? I mean, you've got some music playing, you've got four Firefox tabs opening that are all loading simultaneously, you've got your system 161 thing running, you've got virtual box running, right? That's kind of a big program, right? So part of the job of the operating system is create this illusion that there are multiple things happening at once, okay? Now, on your actual computer right now, are there multiple things that are happening at once? Is that even possible? It is, okay? This is a new answer. It's probably different than it would have been 10 years ago, why? So you have multiple cores, right? 10 years ago, your laptop or whatever dinky machine you're trying to run virtual box on wouldn't have had multiple cores, right? It would have had one core, right? And so in the past, the illusion of concurrency was really an illusion, right? I mean, there was no concurrency on the machine. There was one processor, one processor core, there was only one thing that core was doing at a time. Now, so is this still true? Is this still an illusion? Why? I heard a yes, why is an illusion still? Yeah, because normally you have a lot more things that are going on the machine than you have actual cores, right? Maybe you have like four cores. How many people have two cores? Four, eight, 16, 32, 64? I think I have 64. I don't know, I don't, sort of embarrassed by that actually. I do very little computational intensive stuff on my machine, but whatever, I mean, I'm a professor so I need to have a big fat machine, right? I've earned it. Anyway, so, but it's possible that you, unless you're me, in which some of your cores are probably completely idle, right? You probably are keeping most of those cores pretty busy. And so what's happening at a very short time scale, and we'll talk about this, we'll come back to this when we talk about abstracting the CPU, is that your system is stopping and starting things constantly, right? At a rate that is way faster than you can perceive, right? So constantly what's happening is threads, like a thread, a process is getting stopped, its thread is put away, all the state is saved, a new process gets to run, and that's how, even on a single core system, you know, you can be having a YouTube video playing while your web browser is loading, while you're listening to an Mb3 video, and chatting on Facebook, right? So the trick is, you know, there's a lot of interesting tricks to getting that right. We'll come back to that when we talk about processor scheduling, but that's what's happening, okay? Threads are the abstraction that we use to abstract and multiplex the CPU, right? So a thread is we're gonna use the term in this class, corresponds to the state that's required to stop something from running and restart it later, okay? And I think that's it, right? On the other hand, if I'm talking about something today, or Friday, or Monday, and you guys feel like you have an unusually strong feeling that you don't remember what that is and that we never talked about it, because I mean, some of that's a little normal, please ask, right? Okay, so here's the problem, right? I've created this illusion. I'm able to have a bunch of things that are stopping and starting sort of all over the place, and that's, on one level, that's fantastic, right? It, you know, how many people have written a multi-threaded application, right? The reason you guys do this is because it's a natural way to think about things, right? I have one thread that just works on one thing and another thread that works on another thing, right? In a lot of cases, you don't have to write your application that way. You could write your application as one huge loop, right? I'm serious, right? And there are sometimes performance advantages to doing this. You usually don't actually write it as one big loop, right? There are tools that will help you write it as one big loop, right? But you can actually do that, right? There are event-based frameworks that achieve really, really good performance by not letting you do things in a multi-threaded way. But we do that anyway as programmers, because again, it somehow it matches up in a certain way with how we think about how to structure concurrent applications, or at least how we've been taught to think about it, okay? One of the main reasons that we started Somp Threads is the CPU is the fastest thing on the machine by far, right? And there's a lot of times when your process will be running along, it's doing some computation, but then it does something dumb, like write data to the network, okay? That's gonna take so long, right? The CPU is like, there's no way I'm just gonna sit here while you wait for that network device, you know? That's not smart. So what happens is we stop that thread from running, we give somebody else a chance to use the CPU, right? So this concurrency is a nice thing. It helps us hide the latencies that are caused by parts of the computer that are really slow, which is pretty much every part of the computer other than the CPU, right? And the CPU is the fastest thing. Nothing waits for the CPU, right? The CPU waits for everything else, right? Even memory, right? Memory is slow. Okay, but now that we're gonna have multiple things happening at once, we have problems, right? And the problems of synchronization fall into a couple of categories, right? The first category is how do we coordinate the activity of multiple threads that are running simultaneously, okay? There's cases and we'll go through some examples over the next few days where I have multiple threads that are working on something together and they need to communicate in a way that's effective and safe, okay? Correctness is what we usually think about when we think about synchronization because synchronization problems frequently manifest themselves as unpredictable behavior, right? A case where normally, how many people want their computer to be more unpredictable? Anybody? I mean, it'd be kind of interesting, right? Like once in a while, there was like a, you know, like turn up the unpredictability button in your computer just kind of do, if you would try to go to Google and it would take you to Facebook instead, why not? You know, kind of interesting. Anyway, most of us want our computer to be deterministic and we get frustrated when it's not. If you don't use threads appropriately, you can create indeterministic behavior, undeterministic, non-deterministic, whatever it's called, and people don't like that. Users don't like that, right? Users don't like when they hit a button and half the time one thing happens and the other half something completely different happens. Unless that button has a label on it that says do random thing, right? I don't think there's, I've never seen a button like that. So, most of us want things to work, work reliable, okay? So we're gonna focus on correctness for the first couple lectures, but when we come back to condition variables in particular, we'll talk about coordination, all right? So, here's my question. Why is this coming up in a class on operating systems? How many people have learned about synchronization before? Okay, so it's a good number of you, right? But why are we talking about it in this class? Why do I need to make sure that everybody's heard about it? Yeah. Yeah, so the operating system itself is a program, right? We, I thought hopefully we would establish that by now. And it is one of the more complicated and important to get correct multi-threaded programs that you can run, right? Again, bash doesn't need multiple threads. You can have a completely awesome shell that is single-threaded, right? Your web browser probably needs multiple threads. You would probably be pretty sad if your web browser only used one thread, right? It's possible it could get away with only using one thread. The operating system, I'm not sure you can design a single-threaded operating system, even if you used all of your event-driven foo on it and tried to get it to work, right? So the operating system is inherently multi-threaded, and it's also really important that it be right, right? And, you know, so operating systems not only typically have multiple requests going on from applications that they have to deal with that can happen concurrently, right? But operating systems frequently create their own threads to do things, right? So if you use top and you poke around on your Linux system, you'll frequently find out that Linux has a bunch of threads that don't, that have, and no association with user processes. They're doing things that are useful to Linux, and Linux is using those threads in many ways the same way that an application would, right? For some of the same reasons, okay? Finally, I have lots of shared state, right? The whole, I mean, again, fundamentally I have one CPU, one blob of memory, you know, one or N disks, right? One network connection. The operating system exists to share these resources, right? I'm multiplexing resources, that's my main reason for being, okay? So I've got a lot of shared state, I've got a bunch of threads, this equals synchronization challenges, right? I won't say nightmare, because I think, I wish I could do your assignments again, I think they're awesome. So I think it's not really a nightmare, they're kind of fun, but it's a challenge, right? So this is gonna be something you guys are gonna have to deal with, right? Finally, if the operating system makes mistakes with respect to synchronization, it's over, right? I mean, again, the operating system is a special process, everybody else has to be okay with it being in charge, one of the things applications want is for that special process that they put in charge of the machine to not crash and kill all of them simultaneously, right? Like that is not something that users like, it's not something that applications like, right? So the operating system has to get it right, right? If it doesn't, everybody suffers. Okay, so as we go through the examples today and Friday and Monday, here are the things that you need to keep in mind. When we think about how multiple threads, processes, whatever you wanna call them, access shared resources, okay? Unless you do something about it, you do something about it, maybe you've solved these problems, right? Your threads can be run in any order, they can be stopped and started at any time, okay? And they can remain stopped for an arbitrary period of time, okay? Again, normally these are good things. The operating system is doing this in order to make better use of the underlying resources, but as a programmer, these can be frustrating. For example, things that look atomic to you when you're writing C code are not necessarily atomic with respect to the operating system. So you may think there's no way that my thread can be stopped in the middle of initializing a variable. How many people would think that int i equals zero would be something that I can't stop, get stopped halfway through? But it turns out you can't in certain cases. If that is a 64-bit variable, for example, that requires several machine instructions to initialize, you can get stopped halfway through, okay? So don't make no assumptions, right? That's the safest way to write good multi-threaded code. Don't assume anything. The only assumptions you can make are ones that are based on the synchronization primitives you've applied to the problem. So this is the canonical example that we do every year. So examine this piece of code. Imagine that I work at a bank. I have my very own special bank where I wrote all the code because, you know, if you're a hacker, that's what you do. Here is the code that deposits money into my account. It first calls a function that checks the balance. This function returns the balance which is stored in this variable. The next thing I do, and this is a function that deposits money into my account, right? It takes some account identifier and a large amount, right? And its job is to increase the balance in that account by that amount. I store this variable locally and I increment it by a large amount. And then the next thing I do is I call putBalance and I store the balance in the account. And finally, there's a notification here that sends me a little tweet or whatever and lets me know that I'm richer than I was one tweet ago, right? Okay, so now imagine that there are two of you. Imagine I start with $1,000 and there are two of you that are trying to make concurrent deposits. One of you guys is depositing 1,000, right? That'll get you a B. The other used 2,000, that's more A minus, right? Yeah, I have an exponential scale, right? You want an A, we're talking five figures. So, and then imagine that these deposits are going on concurrently, of course they are. You guys are all depositing money concurrently, all the time into my account, right? That's clearly the common case, right? Okay, so here's what can happen. In the best case, right? So what should the result be? After these two deposits complete, how much money should be in my account? $4,000. If I don't synchronize these two threads, how many different values can this variable take when the deposits are completed? I hear two, do I hear three? I hear three, do I hear four? I've got a four, anyone want to go to five? Okay, let's see, right? So people clearly think that, again, this should be deterministic, right? I hope, otherwise something weird is happening, right? Otherwise you've given me money and somehow money is vanished in the process, right? We don't like that, banks don't like that, banks get in trouble for that, hopefully, unless you're in the stock market in which case it's just whatever. All right, so here's what can happen in the best case, right? My A minus student begins depositing money and they deposit $2,000. And at this point, my balance is now $3,000. The B student comes along later, they deposit $1,000, my balance is now $4,000. Notice that these two deposits did not interleave in any way, right? So this is a case where they were completely separate, right? Imagine one of you guys did it one day and the other didn't the other day. But again, the common case here is you guys are all depositing money at once, right? Because I've notified you on piazzan the night before the exam of this policy, right? So everybody is immediately rushing to their computer to run this code. So here's the less good thing that can happen. So, and remember, unless synchronized, these threads can be stopped at any point. They can remain stopped for an arbitrary period of time, right? Okay, so my A minus student gets the balance, they store it locally and they now, their copy of the balance now has 3,000, okay? However, at this moment, remember, they can get stopped at any moment. So this is a bad moment for them to get stopped, but that this wouldn't happen. Now B student runs. A B student gets the value of the balance and now their local copy contains what value? 2,000, right? Because they got the balance from here, the balance has not changed. At this point, there's two outcomes, right? One is bad for me, the other is even worse, okay? In the not so bad outcome, the B student puts the balance first, which increments it to 2,000 and the A minus student puts the balance next, which increments it to 3,000, okay? This is not the right answer, right? In the very bad case, it starts out the same way, but unfortunately, the A student deposits first, the B student deposits second, right? And now I'm left with only $2,000, okay? Can anyone see why this is called a race condition? Right, because at this point in the execution, there's these two threads are racing. Whoever wins will determine the result and hopefully that's not how my bank system is built, right? Okay, so a race condition we can define as a case where the output of a process is unexpectedly dependent on timing or other events. There's times when we expect the output of a process to depend on timing or other events, but there's other times when we don't. We don't want it to be dependent, okay? And again, the definition of a race condition is entirely based on what we think should actually happen. Here, I should have had $4,000 after both deposits, otherwise I'm violating the law of conservation of money, which again, is something that banks would probably be wise to enforce, okay? So we'll talk for the next few days about two other concepts that I want to define carefully. One is concurrency and the second is atomicity. And to some degree, these are both illusions, right? And these things are at war with each other inside your operating system. Atomicity is frequently required for correctness, concurrency is required for performance. So concurrency, remember, is this illusion that multiple things are happening at once, then I'm achieving by being able to stop and start processes with threads at any moment in time. That's how I get this to happen. And when we back up and talk about the process of abstraction and threads, we'll talk about how this happens. But for now, just believe me that I can accomplish this. I can stop a thread at any moment. And I can start up a new thread and then I can restart that stop thread later. Atomicity is the illusion that a bunch of actions that are actually separate take place all at once. So at the lowest level, the only things that are truly atomic on your computer are little tiny things like increment the value of a variable stored in a register, right? That either has happened or it didn't happen. Anything else, anything more complicated, this code, two lines of C code, not normally atomic because it consists of multiple instructions. And so making it atomic is something that we actually have to engineer that we have to get that to happen. All right, and that requires that we not stop threads at certain points or ensure that threads overlap in specific ways. Okay, I'm gonna skip this. I think we are at a good stopping point. Let me get through critical sections. So in order to achieve atomicity, we use this idea of what's called a critical section. In a critical section, you can think of as a portion of your code where you can guarantee that there's only one thread that will be executing at any point in time. Now remember before with our bank example, we didn't have a critical section and so that piece of code could interleave in any way. But we'll go back to that example either today or Friday and we'll talk about how to set up a critical section to ensure that those couple of instructions that need to be atomic, the getting the balance, incrementing it locally and saving it back, we can now ensure that those don't interleave in the bad ways that we saw previous. Now these sets of instructions to every other thread that's outside the critical section will look atomic. So if I try to get inside that critical section and somebody else is in there, I have to wait till they finish and then I can start. So when you start using critical sections, in critical sections are something that you guys will have to use over and over again as you do assignment two and assignment three. So here's how you identify a critical section. We'll start with this again on Friday. You think, what is the local state that's private to each thread? In this case, what is the private state? What's the public state that both threads are trying to modify? The account balance, right? What's the local state? This local variable that I've created that initially stores the value of them, right? The shared state is the account and now who can identify which lines are in the critical section? Not very many choices, but there's a series of things that have to happen all together. What three things are those? Yeah, so I start here, yep, yep, exactly. So lines two, three and four, these things all have to happen together because we just saw what can happen if they don't. If I get stopped in the middle and someone else can get in here, then what happens is they can get a stale value of the account balance and write it back later. So if I establish my critical section with lines two to four, then I'm good. Now why is line, now, usually people say, well shouldn't line five also be in the critical section? It could be. So if I put line five in the critical section, what would that guarantee? Then I always am notified of the right amount, right? And I suspect that if you run a bank, you probably do consider that to be important, right? Because even if I got two tweets and it was like your balance was 3,000 and then 2,000, that might be alarming, right? And I might call them up and be like, what's going on? No, actually your balance is 4,000, you're good. Our programmer just decided that that didn't need to be inside the critical section, right? Probably wouldn't tell you that on the phone, but anyway. So you could put line five inside the critical section. It's not technically, you might argue it's not required for correctness. I would argue again, if you're running a bank, it probably is, okay? So on Friday, we will talk about what's required of a critical section. We'll go back to this example and we'll talk about locks.