 All right. Hello, everybody. So attendance has noticeably dropped. I'm assuming as everyone in here either like done lab two or haven't done lab two. Oh, we got a thumbs up. Oh, we got. Okay. So this is a group that did lab two and everyone else is still doing it. So people didn't believe me that if you like start on the weekend, you're probably screwed. So I guess they're kind of dealing with that now. So first order of business lab three potentially longer than lab two. So I added a petition. If you would like to have three weeks for it, go vote on it. So go open your discord, hit the little check mark. Yeah. So the coding is a little bit more than lab two. You might find the debugging slightly easier, slightly harder depending on this last year. So you probably want the three weeks for it. So go vote on that so I can actually change the syllabus and hopefully do that soon. Someone unvoted for it. So it needs like 30 more votes on the discord. So check the make sure you have the check mark. That means your vote counts. And yeah. So that will also increase the wait for it, but it will make one lab be one week instead of two weeks and it will be much, much shorter. So with that, let's dive into the next subject threads and then Thursday will have a bit more background. So this is what you're doing for lab two or sorry, lab three. So as an aside for those that haven't used threads before and don't know the terminology. So in computer science, we're a bit weird for normal people that can speak English properly, which is not myself. Concurrency and parallelism kind of mean the same things. If I do things concurrently, that's like the kind of the same thing as doing things in parallel. But since we're computer people, we're weird and they actually represent two different concepts in computing. So concurrency is being able to switch between two or more things. So you can get interrupted, make progress on something a little bit, switch to something else, make progress, switch to something else, go back and forth. And the goal there is to make progress on multiple things over a period of time. While parallelism is doing two or more things at the same time and those two things can happen at the same time, meaning they're independent. And my goal for computing means I run as fast as possible. So if I have eight cores, I want to run them in parallel. That means they're all doing something at the same time instead. So let's have a real-life situation to make sure we understand what the difference is. So you have to bear with this like weird example. So you're sitting at a table for dinner, you have four options of what you can do. You can either eat your food, you can drink, you can talk using your mouth presumably, and you can gesture and say you can gesture with one hand any kind of gesture you can do. And the weird little caveat here is that you're so hungry that if you start eating you just won't stop until you're finished and that's so we can you know discuss concurrency. So picking any of the two tasks, what can be done concurrently and what can be done in parallel. So and also we're assuming we're like polite individuals. So if we take two like say talking and eating at the same time, that would be you can't do them in parallel because you're not supposed to talk with your mouth full. And you also can't do those tasks in concurrently because you can't switch between them because of that weird little caveat that if you start eating you can't stop. So between eating and talking you can't do them in parallel and you can't do them concurrently. What about gesturing and drinking? So could I do those in parallel and so we got a few parallel. Can I do them concurrently? And same hands yeah so for both of those I can do them in parallel or concurrently. I could start talking or sorry what I say drinking gesture yeah so I could start drinking gesture at the same time you know with some skills and I could do them at the same time in the same time instant so I can do them in parallel. I could also decide to stop drinking whenever I want switch to gesturing go back and forth between them. What about eating and drinking? Can I do them in parallel? No one can I do them concurrently. No because that dumb caveat where if I start eating I can't stop so if I start eating I can't switch to drinking because I don't know I guess I have an eight all day or something like that and I also can't do them in parallel because I only have one mouth that's kind of your limiting resource. So does that kind of make sense to everybody the difference between them? So for an exhaustive list here's where it is so you can't eat and talk or drink at the same time and you can't switch so you would consider those tasks to be not parallel and not concurrent with each other. Yep yeah the question is if you're drinking a smoothie does that count and you're just drinking water like yeah not trying to over complicate things here so this also you can eat and gesture at the same time but you can't switch because of that weird caveat sometimes you might encounter this in computing where if you start something you can't resume it again so you just have to do it until completion and then you can drink and talk at the same time and you could switch but you can't do them in parallel you can't do them at the same time because you have one mouth resource pretty much but you could switch between them because you couldn't use your mouth for different things and then you can talk or drink and gesture at the same time and you could switch so those two tasks would be parallel and concurrent so that's going to really matter once we have to you know manage these types of things and we'll see how this applies to us but this is a good thing to keep in mind if you haven't seen threads yet. So what are threads? So we started in processes and threads you can think of just as like a lighter weight processes except by default they're not completely independent and they share memory so they there can be multiple threads within a process and they can execute different pieces of code maybe in parallel maybe concurrently and they're in the same address space so any variable you can access in one you can access in the other but what is independent towards them is they have their own registers because they're supposed to represent virtual CPUs and they have their own stack but aside from that that's the only part that's independent they have their own stack they have their own registers which includes a program counter which is what they're executing but other than that they share the same address space so if you change a global variable in one thread you see it in the other because there's no they have the same virtual addresses the same address space they're all the same so if you want things to be private per thread you can do that and that's something called TLS or thread local storage that is memory allocated specifically for one thread not to be shared with others but in this course you'll probably never see that only once you like start using threads more hardcore so you've been using threads already even if you don't know that so one process can have multiple threads by default a process has a single thread that starts executing things so by default a process just executes code in its own address space and threads allow multiple executions in the same address space so as a consequence of that they're lighter weight less expensive to create than processes and by default they share code data file descriptors everything they live within the same process so knowing what we discussed about page tables you know last week why would creating a thread possibly be a lot faster than creating a process so what's one thing I need oh yep yeah I don't have to copy the page table I don't have to potentially copy memory because they all share by default they use the same page tables so I don't have to copy anything so that's one of the main reasons why it's going to be a lot lot faster so I just have to basically allocate a set of registers and a stack and I'm done so why would I use one why would I use threads especially you know they were a thing back when you only had a single CPU on a machine so you can't do anything in parallel because you only have one physical core but what they're good for is expressing concurrency so you can have multiple different executions going on in your program at the same time that you switch back and forth between so it and if you switch fast enough it actually looks like it's running in parallel when realistically the operating system is just doing context switches fairly fast and it just looks to our slow human brains like it's actually doing going care parallel so why you do this is it might be easier to program concurrently for instance if you know that oh well I want to be juggling a bunch of different things at the same time some might be blocked on files or whatever or slow so one example would be like a web server so this would be like the code in the core of any web server it's just an infinite loop and all it does is try and get a web request like when you connect to a server and then it will create a thread to process that request so every request that comes in would have a new thread which would keep track of that specific execution and you can juggle like thousands and thousands of requests at the same time do a little bit of work on one switch to another do a little bit of work on that switch to another do a little bit of work on that and then finally finish finally finish in that way you can handle multiple web requests so here's like a breakdown they're really similar to processes in the way we use them aside from that default sharing memory thing so with processes remember we had like independent code data and heap between processes but for threads they're going to have shared code shared data and shared heap because they're not independent they exist in the same address space and each process has an independent execution they're completely independent while a thread lives within a process so it is just one essentially thread of execution in a process you can think of it it's like a virtual machine within that process the way that they're similar is well a process has its own stack and registers and so does a thread and or another way to think of it is just a process just has a single thread executing in it by default when you start it so for a process they're going to be expensive to create and we have to context switch between them that's going to be also more expensive because it would as part of context switching pretty much comes down to the page table we have to context switch we have to change address spaces which is going to take some more time than if we don't have to do that and to create it's going to be faster because again we don't have to copy the page tables we don't have to potentially copy memory so threads are a lot cheaper to create and a lot cheaper to context switch for thread you don't have to change the address space all you have to do is swap in some registers and you're done now for a process what we've been covering lab 2 and what we know about processes is they're completely removed by the OS on exit after someone waits on them and that whole process everything associated with it is now gone poof doesn't exist anymore while for a thread if a thread exits within a process the only thing that gets deallocated is its own stack and maybe some you know whatever data you use to store its registers and other than that you know it's not that much data and then as a consequence when a process dies all threads within it dies well so if you have multiple threads within your process and your process dies it brings everything down with it which is one of the way reasons why you know Firefox or Chrome tabs exist in different processes because if they existed in threads well if there's a problem one thread brings down the whole process which brings down your whole web browser which means you're not gonna have a good time so in this course we'll be using POSIX threads and in lab 3 you will be implementing your own threads as well so these are like the default ones that will kind of base that are going to be way more advanced than yours that we'll learn to use and you'll implement a like basic version of threads so for Windows there's like win32 threads but we're going to use POSIX threads it's pretty much supported on everything and to do that it's another fun library it's pthread.h and when you compile so on all these will be given to you but you have to link to that library there's a pthread library so all these threads have documentation the man pages which is nice and fun annoyingly they don't quite follow the C system call wrapper convention so they do a little they have a little bit of a different style when they return errors so when we created processes we forked and then suddenly we had two processes for pthreads it's a bit different so this is the API of a pthread create so you have to give it a thread data structure which has its own internal accounting then the second argument is some attributes you can set to set some options on how you want the thread to behave that will go into a little bit and then the next argument is this weird void star star start routine void star if you don't know see declarations all that basically means is it takes a function that takes as has one argument that is a pointer and returns a pointer back so void stars just sees waves saying it's just a pointer I don't care what it is you can figure it out and then the last argument there is a pointer and that will be passed whenever that start routine starts running which might seem a bit a bit weird so creating threads is a bit different so this is what it looks like so instead of fork again we do pthread create we give it the address of the thread data structure so it can initialize it with whatever it needs to and then we'll just null for the attributes just means give me all the defaults and then as the third argument we say whatever we create this thread we want it to start executing this run function and then we will give it no argument we'll just give it a default null so at the point after pthread create we now have two threads our main thread would continue executing as normally and want to want to just print in main and then we have another thread that would start executing at the beginning of run and it wants to have print f in run so this is like if we had two processes we don't know which one's going to execute first and same with threads so now we have two threads we don't know which one's going to execute first so if we go ahead and run this go back so here's the code again and let's compile and run it so compile so if I run it looks a bit weird because it just prints in main and then stops so if I did this with a process what am I missing so if this was a process am I a good parent hopefully not yeah yeah yeah no I should probably wait on it so in this case what's happening is well my parent is going creates a child which could run print f but the parent just print f in main and then return zero which is like an exit zero and now that process is dead so that other thread wouldn't execute and I didn't wait on you know the thread I created I wasn't a good parent so there is an equivalent of that in p-threads and the equivalent of wait for p-threads is annoyingly called join so in join you specify you know there's no process IDs here so you can't just wait on any one you have to like specifically wait for a thread so if you do p-thread join you specifically wait on one thread and here is an indirect pointer so it's a pointer to a pointer so that whatever address you give it it will write the pointer value returned by the run when it's eventually done and that's a way to get information out of the thread so by default when we had a process it just returned an int like just a number when it was done for threads just to be a little bit more flexible they always just return a pointer so this is how to get the return pointer back yep yeah so you yeah so you can kind of think of that so if we go back to this it kind of looks like an orphan thread but because threads live within the process there's really no concept of orphan threads because they're in your process so instead of being an orphan thread and it being reparented to whatever when this process ends it's dead yep so it's part of the process of the process is gone everything is gone so there's there's a concept we can find an equivalent of like zombie threads so if we don't join a thread we might have a zombie thread but there's really no equivalent of orphan threads because they exist within your process okay so here is our join like these they return zero on success and then directly give you an error no error number otherwise and don't set error no and the rule of this like the rule of weight is you should only call this on each thread or in the case of weight on each process exactly once if you call it multiple times that's undefined behavior as some of you have found out in lab 2 so don't do that same rule for threads so if we wanted to yep so the question is in the vote or sorry in the run function what's the point of taking in void star so it's just a way to pass an argument to the thread function so by default to be as flexible as possible it just takes a pointer so if I wanted to pass a pointer to it well I could set this argument to whatever I want to appear as the first argument of run and because everything's in the same address space you can actually like pass a bunch of different information using a pointer so everything would be accessible yep yep yeah if the parent process dies because threads live within a process all the threads are now done so so if we go ahead and we do p thread join thread and then null just says I don't care about the return value of it like don't bother updating it I don't care so if we do this we should now see in we should see always see main and in run printed whenever we execute this now so because we don't know what order they might be in well we might get the order the other way around if we get unlucky but we don't get unlucky so it always prints it always prints in main and in run but because we're kind of up to the scheduler it could actually print in run and in main we would just have to be like really really unlucky and remember that was due to the completely fair scheduler we talked about before and the main process of hat still has some time left to execute so any questions about that okay and then I'll also have a little demo program so I wrote to show that creating threads is way more expensive than creating processes while I wrote another thing that just creates like 50,000 processes and then tells you how long it takes so if I time this whoops so if I run this this creates 50,000 processes oh yep so more expensive in this case means it will take more memory and it will take longer to run so in this if I just keep track of how long it takes to run if I want to run 50,000 processes well in total time takes me like 14 seconds while if I want to create 50,000 threads well hopefully that takes a lot less lot less time so it still takes a bit of time but it takes about like a third of the time so that was like 4.8 seconds as opposed to 14 seconds so just in time wise it's less expensive to create threads and processes and that makes sense because they all live within the same address space okay so next fun thing is well here's our example that waited properly so we wouldn't have you know we properly wait on our thread there's no parent-child relationship really because everything lives within a process so you can kind of think of whoever created it but the p-thread library doesn't care who created what thread at all so there's no parent-child relationship in this case but you can wait on any thread you want but you still have that rule where you can only wait on a single thread so now we we joined and at the point of the join like same thing with the process the threads resources can be all cleaned up because it essentially read its status and a threads equivalent of status is reading the pointer value it returns at the end so we joined it we could read its return value and we're good so the library can go ahead clean up all the resources associated with that thread now so there's also a thread equivalent of exit thankfully it's not named something different it is called p-thread exit and that's how to return early and it just takes a pointer value that you want to return so and there's also an equivalent of you know main returning zero and that eventually calling exit well if that thread routine returns a pointer value it eventually calls p-thread exit so same thing yep so the question is how does it context switch between threads within a process so when you contact switch processes you like swap all the registers and you swap the address space for switching within threads within a process when you contact switch you only have to swap the registers yeah so we'll get into it later so the thing with threads are because they live within processes they can either be the kernel can either know about them and then just do its normal scheduling thing or the kernel cannot know about them and leave the scheduling up to you and you decide so the kernel will schedule processes if the kernel knows about threads it will also schedule those threads like processes it might just be faster to switch within threads within the same process and we'll go over next lecture more getting into that in a bit more detail yep yeah so if instead of p-thread exit I wrote exit zero what's that going to do so exits a way to exit the process right so if I exit the process everything's now dead yeah so this is one fun thing you'll find that hey if one of your threads decides to call exit somewhere well that entire process is done and all your threads are now no longer running so you have to be careful about doing that so if you accidentally do exit instead p-thread exit you might notice your whole process is now dead yeah so and again that you can think of that oh yep yeah except that it exits a thread instead of the whole process so this is also funnily enough remember the name of our system called like in lecture two is exit group instead of exit and how people thought that was weird well threads were introduced in the kernel much later in time and exit would exit from a single thread and then suddenly processes got multiple threads so they came up with exit group and exit group means the whole all the threads within the process also exit so that's why it's called exit group because it kills everything okay so by default threads kind of behave like processes where you have to wait on them you have to be a good parent but with threads there's something called detached threads because sometimes you might just create a thread and you don't care about return value well with processes you might not care about return value but the kernel doesn't really care about you that much and make sure you check it anyways but with threads you have a bit more leeway so joinable threads are kind of like processes that you have to wait on them or join them and then they release their resources but for detached threads they release their resources as soon as they're done terminating as soon as they call p thread exit all their resources get freed and you don't have to worry about freeing them up you don't have to worry about creating the equivalent of like a zombie thread so if you don't join them yep no so detached thread dies when it calls exit so it's only that thread that dies and it's just dead yeah yeah so this you can say a thread is detached by just calling p thread detached and that will make that thread a detached thread instead of a joinable thread if you don't give it any arguments by default you get a joinable thread which means you have to join it before it actually frees the resources and then like so many fun library functions well if you call p thread detached on an already detached thread that's undefined behavior it's allowed to do whatever and make you know make it impossible for you to debug so now we're faced with the same problem as before so if we go back to that code where we wanted the other thread to run we want to see it print in in run so we joined on it to make sure it was done before we eventually called exit and ended the process well now if I make this a detached thread I can't join it so now I'm kind of in the same situation as before so everyone understand why this probably every time I execute execute it just prints in Maine and nothing else any questions quickly about that okay so why would probably just print in Maine and then stop if I have a detached thread which hopefully it's the same reason so I'll start off by having something that is executing Maine it will create a thread that wants to execute run if it gets chosen to execute and then I detach it which means I don't have to join on it at all it will free its resources whenever it's done and then I print off printf in Maine and then I'd have like an implicit return zero which would exit zero kill the process and then that detached thread won't run oh yep yeah by default threat when you do p thread create with just like a knoll here you get a joinable thread so this by default I get a joinable thread and then I call p thread detach on it and now it's a detached thread so same reason this just prints in Maine because that main thread just calls exit zero kills the process kills the thread so oh yep yeah so if I put a sleep after in Maine yeah so if I put a sleep before or after well likely it's going to always print the other one depending on how big the sleep is because it'll have enough time but you're still up to the kernel where it might not because you can't guarantee anything at that point so I could guarantee it falls asleep for a second but you can't guarantee the kernel will actually execute the other one in the second so like a hundred percent of time that would print both but it also make it slow and it's not the proper fix so yeah you're just kind of arbitrarily slowing things down and hoping that it happens in times which generally unless you're working for a company that's not a good idea if you're working for a company just write like a loop that loops a million times and makes things like really slow and if you want to impress your boss just like sometime few months down the line just delete it and things go faster it's great fun work stuff all right oh yep so detached thread I can't call a join on it so another thing that makes detached threads different than joinable threads is I can never get a return value out of them because I don't join them so nothing can read them so you'd want to create a detached thread so sometimes you might create thousands and thousands of threads and if you don't care about the return value at all well then you may as well make them detach threads because you don't have to remember to call join on every single one of them to release their resources and otherwise you might like slowly just run out of resources over time because nothing gets freed so essentially if you have joinable threads and you don't join them they're like zombie threads and they're gonna waste some resources so that's why you would have them mostly just for convenience yep so so the questions what's the difference between p thread exit and return from the thread function right yeah so that's the same questions what's the difference between returning from main and calling exit no so if I do return from main with like a number that eventually gets passed to exit right like if I return one from main that's the same as exit one so it's the same thing here so if I return null that's the same thing as calling p thread exit null or yeah yeah by default I'll call exit for you by returning from this function so yeah everything in threads nicely has an analogy with processes which is why it's done after so the reason this doesn't print both of them is because well one thread calls exit so if I want oh is there another question yep so this the thread running run would be detached and that calls p thread exit like I could exit I could do a p thread exit as the first line of here if I wanted to but the problem here is my detached thread never executes yeah yeah p thread exit is like the thread killing itself just like exiting from a process is the process killing itself so in this case oh yep yeah in this case before I saw both when I added the join because I'm waiting for that other thread to continue I could read its return value but I don't but I still have to wait for it to stop so I'll always see both prints in this case I don't have a wait anymore so it's like the first problem where I didn't know which they can both execute and just the way the scheduler works is I'll always probably see in Maine and not the other yeah so yeah so the exit group thing is like the system call which is different than the exit call so that's like more low-level stuff so the see confusingly enough the C exit calls the system call exit group which kills everything and no one really uses a system call called exit anymore yeah yeah yeah so the only way to exit a thread is the thread calls exit on p thread exit on itself and that thread is no more or any thread running in the process calls exit which ends the process which ends everything yeah so in this case well for detached thread we might want to wait for every single thread to finish executing before we finish so how we do that is we can add our own p thread exit to the default kind of main thread that was executing Maine so instead of you know hitting the implicit return zero which would call exit well I can call p thread exit here and that would stop execution of the default Maine thread and as part of the behavior of the p thread library well it will wait for every single thread to exit before it exits the process so in this case I have two threads some original thread that got created with my process that starts executing this code and then the thread I create that is starting to execute run and in this likely what's going to happen is as soon as I create it it will the kernel will keep on executing my main thread which will detach the newly created thread print in Maine and then p thread exit itself which will just kill the main thread so it won't exit the process because there's still another thread that wants to run in Maine that wants to execute so now eventually I'm going to exit or exit from my main thread and then the kernel if it wants to run my code has no choice it has to run that the other thread that wants to execute run and if I execute this it will always print in run so let's go back so detach fix so now if I run it no matter how many times I run it I'm always going to see both get printed every single time yep yeah so in this case the main thread always pretty much always ends before the run thread starts executing and that's just the way that just has to do with the scheduler and how it works because remember you essentially get a time slice and the main thread still has some time slice left over before it would try and context switch to the other one which needs to set up and do some other stuff so you always get pretty much always going to see it yeah yeah yeah here I'll show that you have another question yeah in both examples I could get in run and in Maine but without that explicit exit I might just get in Maine and then nothing yep so no so p thread exit kills whatever thread executes that call just like when you exit it kills whatever process that is like I can't kill another process without like sending a kill or something to it no so in this case with with detached threads the way the p thread library works is well it will only exit your process if all the threads are done if all your threads call p thread exit so if I did do something like say I did this well I would have I would one have to get lucky if but potentially I would never see in Maine because what could happen is Maine creates the thread that wants to start running run and the main thread would want to run this afterwards and print f in Maine but it's the scheduler decides to immediately switch well the other thread would just call exit and then the process is dead so all the threads are dead everything's dead yep so in this one like that so this would not do the same thing because it's not like a function p thread knows about it's your default C main and you don't change the C main so the C main still has the same rules that if you exit from that that's the same as calling exit so it's the same rules like this is exit one same as yeah the previous example without the exit it's just the detached air yeah yeah so in this case it could print them both if we get lucky but what happens most of the time is the main thread creates it keeps on executing print fs in Maine and then return zero which is again the same as exit zero and then the process is done and the other thread doesn't execute yep so question if I run this it prints in run twice okay I'll have to look at that later so I see two in runs so I haven't seen that yet but we'll see I'll try and look at that later okay yep so where the by default like processes when you exit them they just want a number for p threads they want a pointer no so you can only so the only default returning a number and exiting the process is only returning from main and if you want to you know exit a thread well you have to return from that thread function that's executed but no other functions like they don't magically know that hey you return to pointer so you want to exit the thread it's only the function you specify so so if you just return a number here it doesn't magically know that that's supposed to represent a process that would just still return a pointer and then force a one into it which would be meaningless so the the only way it matters is it's the function specified by p thread create and it's a run and it's supposed to return a pointer so whatever that function returns will be the same as p thread exit well and only that function so yeah the only default exit of the process is returning from main like it has been forever and it's like you can make another function that returns an it it doesn't exit from doesn't exit your process right yeah yeah so if I do like p thread if I do something like this I would never see the printf of cool because the thread would have been dead by then so that's essentially dead code so you can write whatever yeah okay so I answered the parent thing too okay so quick bit of fun so there's some attributes you can play with with p threads will probably always just use the default but this might help for to give you an idea of everything has a size limit and everything you know yeah everything has a size limit so you can see how big of a stack it will allocate for you and you can change it if you want or just accept the default so if you write code like this it's like a weird C way of actually accessing stuff that you should probably get used to if you do this you'll see that the stack size is just 8 megabyte so whenever you create a new thread you get an 8 megabyte stack associated with it which is private to that thread and you could also use this attributes if you want instead of calling p thread detach well you can create one of those attribute objects and then you could you know set p thread create joinable on it or create detached on it and then you don't have to have the separate p thread create and then p thread detach you could just give it as an arch or as an argument and attributes but p thread detach is so common that they just made a nice little helper function so you can call that instead instead of dealing with all of this crap so if we compare well we can look at this real quick creating multiple threads as opposed to creating multiple forks so let's go ahead and see that so here's the example and on so in main by default again you can think of there's only one thread running the main typically you just call it the main thread and what it's going to do is instead of that forking stuff that we saw before which is actually somewhat hard to read this will go ahead go into this for loop four times and create a new thread each with an ID and then at the end we will p thread exit so we can make sure that all the other threads actually exit so in here we're going to actually pass an argument to a thread and because everything shares the same memory space we can just malloc stuff so our main thread would call this malloc an int which gives us back a pointer and we will write the current ID to and dereference that pointer and write the current ID to that so that that and then pass that pointer to the thread so only that thread can access that pointer when we create a thread data structure here we create a new thread give it some default attributes we tell it to start executing run and we will set we'll give it that pointer argument which is whatever malloc returned yep so I need the malloc on an int here because I need to pass it a pointer and I need some fresh memory to pass to it so if so we can go over this what would happen if you didn't do that and it's bad so we can do that as an extra thing tomorrow here I'll finish it quick and then tomorrow we can catch up and see all the bad things that happen with this so in run it dereferences that int to get the ID back out of it freeze it because we don't need anymore and then it will print the thread ID and do this ten times so if we execute this quick before we go we see that it's kind of like the multiple process example where we'll see you know IDs see each thread print one two three four five all the way to nine or zero all the way to nine but the order between the threads will be all different so here we got one two four three one two four three and for extra fun look at that code and see all the bad things that we've done because we did a lot of bad things there so just remember pulling for you we're on this together and go vote on the thing so I can post the lab with a later due date all right