 All right, good morning. We're unfortunately getting down to a sparse class. That's not good, because this stuff is going to definitely be on the quiz. And this is us catching up to the other lecture and hopefully getting way ahead of them, because we'll learn how to create threads today. So no one's used threads before, right? Some-ish? OK, well, so we figured out processes. That was kind of mind-bending. We've done all the hard stuff now. So threads at this point are easy. So we can just relate threads back to what we learned before. So threads are like processes with shared memory, which is like what we expected before. So now that we know what an address space is, it's essentially like virtual CPUs within the same address space. So they see the same view of memory. They can affect each other. But otherwise, they can run in parallel or concurrently. But otherwise, exact same as a process, except that they just share memory instead of being completely independent. What they don't share is anything that acts like a virtual CPU. So each thread would have its own registers, its own stack, its own program counter, and all of that. But again, same address space. So if you changed some memory address, it would appear in the other thread. If you want to have the behavior where you have independent memory, you can do that. But it's optional. And that's something called thread local storage, which you'll see later. Yep. Yeah, so threads live in a process. But multiple threads can live in the same process. So they all share the same memory. No, so that's just its own part of memory that other threads shouldn't be able to touch. But they still could if they want to. But it should be independent. Yeah. Yeah, so threads live within a process. And by default, so far what we've seen, when a thread is created, or sorry, when a process is created, it only has a single thread in it. And then today, we're going to introduce multiple threads into our processes. So this is exactly what we said. One process can have multiple threads by default. Processes haven't changed. They execute in their own address space. And you can think of a process as, by default, just containing a single thread that acts as that virtual CPU that actually executes your code. But threads allow multiple executions in the same address space. So this would be good if we want to actually share memory more easily and do a lot of communication. We can just do it within the same address space without essentially having to ask the kernel for permission for every single byte we want to pass between threads. So as a consequence of that, they're also lighter weight and less expensive to create than processes because they share memory, so you don't have to copy any memory. You don't have to do any neat little tricks. You just create a new set of registers and a new stack for it. And then off you can go. While if you create a process, you also have to do that to create a new virtual CPU for a process. But you have to also copy the memory. You have to make a new address space for it. You have to clone a whole bunch of other stuff. So it's just way more involved to make a process. So let's use a bit of practice and see what that kind of looks like. So just to illustrate that, so I just have a loop that creates n forks, which in this case is 50,000. And all it does is do some error checking. The child just immediately exits, and then in the parent it just waits. So it'll create a process, let it die, create a process, let it die, create a process, let it die, so on and so forth, 50,000 times. So if we time that, we can see how long this takes to create 50,000 processes. We'll probably be here for a little bit. So every time through the loop, we're going to make a call fork. So it makes one child. And then in the child, it exits. And then in the parent, it will wait PID. So it will just wait for that one child to die. So create a process, wait for it to die, create a process, wait for it to die. So there's only going to be one child active at any time because we're actually waiting for it. So if we do that, that took a total of almost 13 seconds. Later, we'll figure out how to create threads in a bit, but just to time it without showing you the code. We can create threads, and hopefully it's a lot faster, or otherwise this is going to be embarrassing. And there we go. So 5.2 seconds, it's a lot faster to create threads than just processes. And we'll let's go ahead and figure out how to create threads. Oh, yeah, sorry. Just as an aside before, so assuming you just have a single CPU because threads were useful before we had multicore machines. So even a one thread running in one process, it's still useful because they express things like concurrency. For example, if you have a web server, you might write something like this. So in your main code, you might just have an infinite loop that just tries to get a request that comes in like a new user's connected. And then you just create a new thread to handle that request that can run concurrently. So whenever there's free CPU time, you can do a little bit of this, a little bit of that, switch back and forth, right? Instead of trying to write some queue that has to manage a whole bunch of different requests and saving what one you have to resume and everything like that, it's easier. You just use a thread and all that's taking care of for you. So it is a lot easier to program some examples. Yeah. So it's exactly processes that kernel gets to decide. Yeah. Yeah. Yeah. So the threads are like the virtual CPU thing, which actually executes code. So if we bake down a process by default, this has one thread. So really, it just picks what thread to run. And that's it. So there's different ways of running threads that we'll see in the course. There's like kernel-supported threads and then user-supported threads where you write the threading library, in which case it can only be concurrent because there's only one. The kernel only cares about one thing executing, so it wouldn't run anything in parallel. And that's what you'll be doing for Lab 2. So Lab 2, you'll be creating your own threading library, which because they're user threads and the kernel doesn't know about them, it's just going to be like the same rule, one process, one thread, and then you're creating like your own threads to try and fake that. But it won't be able to run in parallel just concurrently. Yeah, so that's what you have to look forward to in Lab 2. But thread just contains its own stack and registers, and which seems simple enough, but you'll see the details get kind of tricky. So here is our comparison between a process and a thread. So a process, of course, has independent code, data heap, everything that would live in its address space, while a thread, all threads within the same process operate in the same address space. So they have shared code, data heap, that you don't have to do any explicit sharing or anything like that, you get it essentially for free. A process has independent execution, so whatever it executes doesn't affect any other process, but a thread lives within a process, so any changes they make to memory would affect each other because they're in the same address space. But both processes and threads have their own stack and registers. While processes are expensive to create and they also have to context switch, threads also have to context switch, but processes are more expensive to create because it at least has to create that independent address space, which we don't know any tricks for it, so for now we can think of creating the address space as just copying all the memory. So it has to do some tricks with memory, while if you create a thread, you don't have to do anything. And then when a process exits, it's completely removed from the operating system on exit, which is like wishy-washy true, because we know that just creates a zombie and we have something else to wait on it. But when a thread exits by default, at the minimum you have to do to remove a thread is just remove its stack and that's it. So because a thread lives in a process, even if you have multiple threads in your process, kernel supported or your own that you will create, wherever the process dies, all your threads are dead. Yeah. So context switching is when you switch whatever is currently running on your CPU. So remember the context switch is like, hey, I need to save all the registers from this process and then load them from the other process so I can switch what's running and that's what the kernel does. So it is how the kernel changes what process is currently running. So concurrency is just being able to change between two tasks and make progress at the same time. So the context switch would be how you have concurrency between two processes. So you just have to keep on switching back and forth between them. So context switching is correct. Yeah, context switching is the mechanism of actually switching what process is running. So it'd be like at least saving the registers from one process loading where the other one left off and letting it execute. Yep. Okay, sweet. Okay, so at least every same person, I guess except for you guys because you'll be making your own threads uses POSIX threads, right? It's just a threading library that's provided for you and it's called pthreads if you shorten it. On Windows, there's Win32 threads but that's like a whole different... Conceptually, it's the same thing but the programming interface is pretty terrible. You can even use pthreads on Windows if you want. So we'll just look at pthreads. Of course, it's C. So you just include pthread.h and then when you give this flag to your compiler and linker to use pthread libraries but they'll always be given to you if you need to use pthreads. And they're like everything else. You can use the man pages to see documentation. So the, yep. It's a man tool. It would be in man three because it's a C library. So system calls, you just have to kind of look them up and know what they are. Like write, read or system calls. pthread creates a threading library so it's not a system call because it transitions from user space to kernel space. So it's the only way to talk to the kernel through system calls. Okay, so pthread create looks a bit different than fork. Remember in fork, as soon as the fork happens it splits itself off into two and then starts executing at the exact same spot. The thread interface is a bit different. So it takes a pointer to a thread to initialize a data stretcher that keeps track of everything associated with the thread which you get to create in lab two but that's given to you for pthreads and then there's this pointer to an attribute that's kind of set different aspects about a thread. It's basically all the options you can set and then there's this fun definition or fun type. All that is is a pointer. A pointer, well, you don't even have to care about this part. So it takes as an argument a function that has a single argument which is a void pointer which again is C's way of saying it's a pointer I don't care what it is and then it just returns another pointer at the end of it and since everything's in the same address space that pointer would still be valid and you can pass it around. And then the last argument is a pointer called arg and that will be whatever you call pthread create that argument will be passed to that whatever the start routine function is. And yeah, yeah, yeah. If you wanna pass multiple things to the function you just make a big a struct, allocate some memory to it and then just pass the pointer which is why we often know pointers, right? And C interfaces typically look like this that they're complicated they just give you a pointer and they say do whatever you want with it. All right, so here's what it looks like kind of concretely. So we have to make a function that takes a pointer returns a pointer and in this case you can kind of think of the function you define as a main for a thread if you want except it doesn't return an int, it returns a pointer and by default instead of returning zero you'd return null as kind of the default I don't really care value. So in here there's just a run that's printfs that's in run and a main what main does is just allocate some space for a thread then it calls create gives the address of that structure as the first argument we'll just use default argument so you can set the second one as null then you can give the address of run you can also not give the address of run you can just give run and C will actually this is one of the things C will actually figure out for you and then we just won't give it an argument we'll just say null again and we'll print in main. So this what are some differences between this and creating a new process? Yeah, 10 lines of code, yeah, it's kind of shorter. Does this make more sense than making a process? As soon as pthread create happens then I have two things executing one will still be going here our original one and then the new one will start executing there. Yeah. Yeah, so that's a good question. The question was, so I won't be able to wait until the thread dies. We'll see how to do that because funnily enough everything we learn about processes about waiting and all that fun stuff also applies to threads. So there's pretty much an equivalent of everything. So that function we wrote there is kind of the equivalent of main and returning from that is essentially killing our thread as opposed to killing our process. Yeah, so what we were missing is kind of the equivalent of weight. So the weight equivalent for threads is annoyingly called join. So there is pthread join that takes a thread and then has a pointer to a pointer for the return value. So that is more in direction. So it just wants an address to store a pointer return value from. So that's why there's two pointers. So it wants to write that to whatever that address is which might hurt your brains a little bit but I swear it makes sense. So it basically just stores the exit status of a thread set by pthread exit. So there's an equivalent of exit. So to the location pointed out by return value. There's something called a cancel thread. So that's like an error but we can kind of ignore that for now. And if it's null, it's just kind of ignored. So you don't have to capture the return value if you don't want to. And that will be capturing whatever that thread exited with. So yeah, for every thread too, there's a rule. You can only ever call join once on a thread and then after that thread is joined it's like it was weighted on in a process. The OS or the threading library can just straight up delete it at that point. So multiple calls in the same thread is undefined behavior. So your computer might get reformatted according to the C spec. So here is a full example that actually weights properly. So it's the exact same thing. So I create the thread I had before and then I go ahead and join it at the end. And I should have, yeah. So in this thread, since we joined the thread resources are cleaned up. We don't have any. You can think of them as zombie threads. They don't really give them a name but there's an exact equivalent between threads and processes. So hopefully this makes sense or at least reinforces what we already learned about processes. So yeah. You said the join is like weight though. Yeah, yeah. So now whatever thread executes the join is waiting for this thread to die. And this thread is the one that is running, run. Yeah, I'll show an example of that. Just in one second. Yeah, so we'll see an example of that. But first, because there's different types of threads annoyingly. But so there's also an equivalent of exit for threads. Thankfully it's also called exit. So it makes it easier to remember. So there is a P thread exit and it just takes a return value. So what you want to exit immediately and that's passed to, you know, that's returned as part of the P thread join. So if your routine just goes ahead and returns normally, that's the equivalent of calling P thread exit, like just how in main, if you return a number from main, that's the equivalent of calling exit with that number. So it works exactly the same. And if you don't, right, exactly like before, implicitly it's called at the end of your whatever function your thread's running. So annoyingly there's also, so that whole zombie thread being able to join on everything kind of gets annoying at some point where you might want to just create a thread and you don't care when it finishes and you don't care to clean up the resources. So there is a thing for threads called detach threads, which might make your life a bit easier. So the default threads type is called a joinable thread which kind of operate exactly like processes. So you have to join or wait on them to clean up their resources. While detach threads, nicely for us, will just release their resources whenever they return, whenever they've finished. And of course, since that happens, you can't do anything with their return value, right? You can't capture it. You can't do anything with it. It's just, as soon as it's done executing, you have no idea when that happens. It releases its resources. So you do that with pthread detach on the thread and then it marks the thread as detached. Then when the thread exits, it goes ahead, cleans up all its resources. And if you call detached on a detached thread, again, it's fun undefined behavior. Yeah, so there's a question on chat. Why does the join have to be there? It's the same reason the wait has to be there. So you can actually get the return value before all the resources get freed up. Then another thing about quiz one, so there's another question about quiz one, whenever I see the quiz, I will let you know. But this stuff is all fair game for the quiz, except we'll hopefully know more than the other sections. All right, so here is an example that I'll run and we can see an actual example of this. So in main, I will create a thread that executes run and then I will call it, I will make a detached thread. So after the pthreads create, there's two threads running. The main thread would go ahead and printf in main and then the detached thread should go ahead and print off and just print off in run. So when I execute this, I should see in main and in run. So let's go ahead and execute this in it. So if I execute that, oops, it just prints off in main and the other thread doesn't appear to run at all. Anyone hazard a guess as to what is happening here? Yeah, so the main process just ended, so it just kills all the threads. So the other thread may have been created, but it didn't have a chance to execute yet or it didn't even have a chance to execute at all and then the process is dead and it goes ahead and just ends all the threads because now the process is dead. So this would have been avoided if we had a joinable thread and then we joined on it because it would have waited for the thread to complete before we exited, but you might get into this situation where, hey, I actually want my detached threads to finish running before my program exits. So what you can actually do is you can call pthread exit from the main thread at the end and what that will do is it will exit the main thread but pthread exit will wait for every other thread to finish executing and have the resources cleaned up before it actually calls exit from the process. So if we go ahead and execute that, we hopefully should get what we expect. So now every time we run it says in main, in run and that's happening from two different threads. So that's one thing you have to be cognizant of when you have detached threads because if you don't join them, you don't know when they die and they could not be dead yet by the time you exit the main thread in which case process dies, all the threads die. Yeah, so the question is, I guess, is in main printed every single time? So the answer to that is, in this case, it's pretty much printed every single time but it's only the way the Linux scheduler works. In one of our examples before, we had a child that executed before a parent that's usually actually pretty rare. If I run this probably like a thousand times, it'll always print this but one time I might get lucky and it prints the other way around. But as soon as you create threads, just like as soon as you create two different processes, you are up to the mercy of the Linux scheduler. So it gets to pick what runs, you don't get to say anything about it. So this, so P threads will run on multiple CPUs if you want. Yeah, this is multiple CPUs. But I have no idea if both of the threads were actually running on the same CPU or different CPUs. So the kernel just hides all that from you. So I have no idea. Generally, the kernel developers are smart people and they'll try, if there's two running processes and nothing else very important, they'll put it on two different cores but not necessarily true. Yeah. Yeah, so detached threads are kind of more convenient because they just release your resources for you and it's pretty great not to have free but the problem with detached threads is you don't know when they're done and you also can't get a return value out of them. So if you actually care about what the thread's doing as opposed to just whenever you're done, you're done, I don't care then you'd want to use a joinable thread. Yeah. Yeah. Yeah, it's exactly like that. But instead of being an int, it's a pointer. There's like some special return values if there's an error like that canceled one but generally you shouldn't see it but it's all in the man papers. But there's special return values but for this course, especially since lab one two you're just creating your own library you don't have to worry about it. Yeah. So the return any errors from pthread is gonna just return through that mechanism but if it's something that affects the process so if it's a segfault or something like that, well segfault we saw gets a signal kills the process so all the threads are gonna die too. But yeah. So this is where we figure out what exit group does. So exit group will end the entire process and every single thread in the process. Exit by itself is a system called a end of thread and only a thread without the process. Yeah. Which annoyingly the system called exit corresponds to pthread exit in this case and the C exit corresponds to exit group. So exit group is like everything dies. So if a thread calls exit group it's like any other thing that called exit group it doesn't care. The exit group just kills the process straight up. Yeah, so yep. So if you have pthread exit in the main thread it will wait for all the detached threads but and wait for all the threads to finish. It's like a special case of it because otherwise if it's in a thread you create that calls pthread exit it just finishes that thread. If it's detached all of its resources are gone. If it's joinable it'll be like in the equivalent zombie process state where you have to join it. Yeah, so that's where the issue comes. We'll talk next lecture. I guess we're skipping ahead a bit about kernel threads and user threads. So if you're a user thread and the kernel just knows that hey your process just has a single thread in it and you yourself which what you'll be doing is creating a bunch of threads and your threads the kernel doesn't have any way to know about them so you only have a single thread to the kernel in your process even though you might represent wanna like context switch between them and run multiple threads but if one of those threads calls sleep because the kernel doesn't know about it your whole process goes to sleep so your threads can't do anything. So that's one of the main drawbacks of having user threads which you'll be implementing. So if one of your threads calls sleep it's done so your whole application sleeps. No pthread exits for both type of threads. So if you pthread exit a detached threads it'll clean up all the resources. If you pthread exit a joinable thread it'll be like in that zombie thread state where it's waiting to be joined until it cleans up all the resources. Yep, yep, exactly. Okay, so we can think of more fun interactions at the end but here's just another example just to show you that I'm not lying and to show you some good defaults probably for your own threading library. So threads have their own stack so you can kind of manipulate this attribute variable and also just get the stack size and kind of just query a bunch of things about the property, the default values for all the threads. So one of the things is to query the stack size which just returns a size T but you have to do all the C stuff so you create an attribute structure called attributes you initialize it because it's C and then you get the stack size so you pass a pointer and then you pass a pointer for that function to write the stack size too and then you can actually go ahead and print the stack size. So as long as I'm not lying the default stack size is surprisingly small maybe or not surprisingly but by default it's eight megabytes and you can also use this attributes interface to set a thread to joinable or detached so you don't have to call pthread detach it just is as soon as you execute it and yeah attributes are basically just options you can set for threads so there's a question in the chat. So let's go ahead and see that so in stack size while I'm doing is querying what the default stack size is. So if I do that says stack size that's eight megabytes right which should be two to the power of what's eight 23 yeah so eight megabytes. So that's your default stack size there's nothing anything special so whenever you create a thread it's gonna allocate some stack that's supposed to be just for that thread and then it will save some registers somewhere and then have to switch back and forth between them. What? Yeah, what's wrong with 3.10? Oh, no that yeah use old stuff. Okay, so let's compare it to creating threads to processes so in lecture four we had our create our multiple process example that created four processes that ran that just printed 10 times so let's go ahead and do this and we'll also see if we can identify a fix to make our program even better. So here's our multiple thread example so this will demonstrate how we pass an argument because remember before when we were using fork we just passed the ID and it was just copied at the time we forked and we didn't have to do any manipulation with it because after that point it was independent. Well, now we have to pass stuff through pointers if we want to pass arguments to a thread so if I wanna pass an int argument going to have to malloc, or sorry, yeah I'm gonna have to malloc the size of an int so this is how I create a new thread every time through the loop. I'll malloc space for an int and then I will dereference that to write the argument to it so now in my thread I have to know that I passed an integer pointer so you get to cast it back and all that fun stuff but in the thread I will create a thread so I'll give it the address of a thread then yeah so instead you wanna use the space for the pointer to shove in an integer where here, you want to essentially you want to instead of passing arg you just wanna do that. Okay, so let's review, let's review first year C not as an insult just to see what would happen cause this is actually a good point and this happens all the time. So let's think about what would happen again wasn't insulting and sorry if it sounded like that but this is so in C, int id that's passed by copy right and it's and it lives as long as this function lives right? So the address of id is valid as long as this function is alive, correct? Okay well if I call p thread create I don't know why there's an error there it will create a new thread and then this function would exit right this function would return and now that memory associated with that id now doesn't exist. So if it tried to read it, wait why is this an error? Oh cause I doubt it, I doubt it. So if I do that in the thread whenever it would execute at some point that address for the id is not going to be valid anymore or if I get lucky the address of that id is going to be some other value because everything lives in the same address space right? But let's just execute for fun, yeah. So invalid pointer because it doesn't exist anymore and this is actually a nicer error message than I thought it was going to get but does that make sense? Because that id only lives as long as this function so here I'll go back to the start. So in main all I'm doing is like the multiple process example I loop through it four times and essentially just call new thread and then at the end I'll do a pthread exit cause I wanted to wait for everything. Then in new thread if it takes the id it's passed essentially it'll live essentially on the stack of the main thread it will call pthread create give the address of that which was allocated by the main thread you'll hit return and then that address is no longer valid and then the thread would try and read that address it's be invalid memory, right? So this is actually a common issue like it's not a stupid question people sometimes forget that and will return a pointer that is only allocated on a stack that doesn't live long enough to be used by something else especially if you've threads using the same address space it kind of gets confusing especially if you think to say so you'll figure it out it lives as long as that pointer or that function lives and it'll live long enough, yeah. But we exit from this? Yeah, so well within run within the thread it's gonna do this and try and read the integer I was even, this code is even nice because it tries to grab that value as soon as possible instead of reading it later but all it does, oh I guess it frees here whoops that's probably my error okay now I should seg fault yeah so in run what I had before I would free it because I knew I malloc'd it beforehand but in this case if it just kind of lives on the stack and I give it the argument I'll go ahead and try this now so this should be more fair so it's like some big garbage value so instead of being an invalid pointer because I tried to free it it's now just some garbage value because it would got returned that address represents some place in the stack in main but it just keeps on getting reused and every thread is now going to every thread got the same address because it would have done the same function call and actually went to the same address every time so every thread gets the same address and then it gets reset to some garbage and then they would all have the same address so even more confusing than just crashing it just gives you some garbage and you have no idea where it came from so if we go back and remove or add back in the free whoops what the hell so let's add back in the free so in this case I'm going to alloc memory because I want that memory to exist as long as the thread exists or as long as the thread needs it for so I'll allocate it in the main thread I'll pass it as an argument and then it's just a pointer I don't care so I'll pass it as that value and then in the run because you're writing threads write threads only live within a process you know what the type is of the pointer the default C library can't have 10 billion different or create threads for every type of pointer you have for it so you just have to cast it so here I know that arg was an integer pointer so I just cast it to an integer and then dereference it and then I should get the ID back out of it then at that point I don't need that memory I'm essentially just moving it from the heap I use the heap to pass the variable and I move it to a stack at this point the stack is local to the thread so now it has its own stack so no one can monkey around with that so after that I can free I don't need this stupid heap anymore and then I'll go like in the multiple process example I'll go 10 times and then just print what the thread ID is and then the number multiple so if I do that I get the output I expect so it will print 0, 1, 2 and the order between the threads in this case it might be consistent but it doesn't go thread 1, 2, 3, 4 in this case it went 4, 2, 3 4, 2, 3 4, 2, 3, 1 and go on and then at that point it's up to the kernel it can schedule it however way it wants we execute different we get a way different output which is still valid so each thread will print off 0, 1, 2, 9 and the same order every time but between different threads you're at the mercy of the scheduler and you have no idea what order it's going to print yep yeah so let's go back to main so that is the slight improvement we can do so technically so main just calls new thread 4 times and here we just do pthread create so the default is it's joinable right so that's the improvement we could do it's joinable so whenever it's done it's actually wasting resources around so we would actually be better served since we don't care about the return value to actually pthread detach but this is like a insane micro optimization right because I have an exit at main so I'm waiting for every thread to die anyways alternatively right I could have kept track of the data structure I had for each thread and then join them all at the end if I really wanted to but I don't care about the return value you're better off using detach so that's like but in this case this is like a hella a plus plus micro optimization so if you're asking that question you're fine for threads for like pretty much the rest of the course well not really but yeah yep yep yep no I'm just not keeping track of the structure explicitly anymore but it's it just keeps running but I lost that data structure that represents the thread but so it means I can't join it again but it still exists it's still running as soon as you created it's still running you can keep track of it mostly just if you want to join it again or if you want to set attributes of it later but it if you yeah that's a good question because as soon as that p-thread type goes out of scope it's gone but it doesn't matter the thread still lives yeah so can you set a detach thread to joinable? yeah you could essentially set it through like this argument yeah so you can have it detach immediately and if someone remember has set play with the stupid arguments thing p-thread detach makes it easy any questions about this example it looks it's pretty much exactly the same as a process example yeah the multiple process example so if you understand that you should understand this yeah any other questions so the argument so this function just takes a pointer as an argument and whatever you give us the fourth argument to p-thread create it gets passed to that function so in this case I'm passing the pointer that I malocked so that's why in here I cast it back to an int pointer because I know what it is and then I free it so I'm just freeing the exact same address I malocked alright well here's a brain buster so we had a few questions about hey what if my thread calls like exit or something like that ok well here's some fun what happens if your thread calls fork oh oh gg's ok well here's an easier one so if you have a process that has 10 threads what happens when you get a signal so these silly questions boring systems are hard so I'll leave you to think on those a little bit but to wrap up today at least by default threads enable concurrency even user threads which you'll be implementing and we're going to talk more about threads next lecture but by default threads at least enable concurrency now p-threads also enable paralyzation but that's not true of every thread it won't be true of yours but threads enable concurrency so we explored them related them to something we already know so there's equivalence for pretty much everything but we know that they're lighter weight they only contain the information of like a virtual CPU but otherwise they don't do anything in memory they act they live in the same address space and then each process has multiple threads but by default a process only has one thread when it starts so pulling for you we're all in this together