 If you go to my learn Unity repo and look here under ECS jobs, you'll find I've been writing this introduction to these two major new features of Unity called the job system and ECS, which stands for entity component system. And these features, ECS in particular, are not fully formed at this point and I'm still learning them. A lot of people are still learning them. So this video is gonna be something of a rough draft in which I just walk you through the basic ideas of both these things, starting with the job system. So the idea of the job system is that rather than creating and managing your own threads, you will create these units of work called jobs and you hand them off to Unity, they get put on a queue and the job system just periodically looks at the queue and if there's a worker thread available, it'll take a job off the queue and run it on that thread. And be clear, this is not a preemptive system so that job once it's running on the thread will run to completion. But once it completes, that thread is then freed up and the job system will just pull another thing off the queue if there are more jobs waiting on the queue. So because the job system is not preemptive, it's not really appropriate for IO stuff. If you wanna do like IO stuff in the background, say a load of file, you would want to run in a thread where if the thread blocks, well, say reading from a file, that's not a big deal because another threads can just take over the core. But here we have a fixed pool of threads. There's a thread that Unity maintains for every core in your system. So like in my system, I've got cores. There's one thread that runs the main core thread, there's a graphics thread and then because I have eight cores I'm gonna have six worker threads. And so jobs usually are gonna run in those worker threads. So if you had, say, a block running in a worker thread and it blocked on IO work, that worker thread would just be out of commission for quite a while and the core it runs on would then just not be utilized for that time. So that of course is bad if we're trying to maximize use of our cores. So it's not really appropriate for IO tasks, it's for computation stuff, it's for you wanna do in-memory transformations of data. That's what it's all about. And the reason for such jobs is you don't want to just create and manage your own threads. Well, a few reasons. One is it's just, it's bothersome and error prone to have to create and manage your own threads. If you don't get it exactly right then you're not gonna maximize performance and bad things can happen. Also just the threads you create are gonna contend with each other and also with Unity's own threads to use the CPU. They have to contend with each other for CPU time. So ideally we just have a fixed set of threads, the fixed pool of Unity threads and we would just farm work out to those existing threads rather than pay the overhead cost of context switching between threads, right? And then another very important reason you would wanna use the job system is because it generally makes it easier to both avoid and detect race conditions which in conventional multi-threaded programming can be very difficult to do. So here in this model behavior class we're defining a job as a struct test job which implements an interface called iJob which is coming from the unity.jobs namespace. And this interface requires one method execute which takes nothing and returns nothing. And this of course is the method that's called when the job is executed, it is the actual business of the job. And in this case it will take the two fields X and Y, add them together, store them in X and then print out the value of X. And then down here we're gonna actually create an instance of this job and run it. So we create an instance of test job, initialize three to X and five to Y, schedule it. Schedule is an extension method of iJob, extension methods and C-sharper methods which are concretely defined in an interface and then implementers automatically get that concrete implementation. So we didn't write schedules provided for us. Anyway, this method puts a job on the job system queue and we get back a handle, job handle representing the scheduled job. But when we queue a job it actually isn't really ready for execution, it's not marked as ready to go. We have to call this method schedule batch jobs static method of job handle. And that will actually then ready all unreadyed jobs on the queue. So the reason for the separation is just typically we often will want to create many jobs and then okay them for execution. If they just immediately started running then the job system would immediately start executing that first job that you happen to schedule rather than making a smarter choice of maybe picking a better one to run first, things like that. So typically you might wanna queue up a bunch of scheduled jobs then kick off their execution. That's why schedule does not actually directly ready the jobs. Anyway, so at some point later then having kicked off execution of your jobs they then at any point might depend upon what's happening on the worker threads. At some point they'll be taken off the queue and executed on one of the worker threads. And then at some point though we need to call complete on the handle. And what complete will do it will wait for the job to finish if it hasn't already finished. It's possible by the time we get down here that the job happens to already have finished executing that could happen. But if it doesn't it'll sit here and wait until it does. And so when complete returns you know that job for sure you know it's finished executing and complete also removes internal references to the job. So it cleans up resources. So that's why you wanna call complete both to remove all internal references to a scheduled job but also there's that point in your logic where you want to make sure that for sure past this point that thing I scheduled earlier has completed. So that's what complete is about. And another important thing to mention here is that when you schedule and complete jobs you can only do so on the main thread. It is not allowed to schedule jobs or complete them within other jobs. That is something they could have allowed but it gets very error prone and it was decided that it's as a rule best to synchronize all your work to create all your jobs and complete them on the main thread. So you can't schedule and complete your jobs within jobs themselves. Another thing to note here is that when you complete on the main thread if it happens to be the case of the job you're sitting and waiting for that you're trying to complete if it hasn't actually started executing yet well then the main thread is just gonna be sitting and waiting. So the job system might decide well the main thread should then run the job because otherwise it would just be a waste of a core. So there are cases where jobs actually won't run on a worker thread. They might in some cases run on the main thread. Anyway so after this complete call we know that the job has finished and what we should see on the console well the job will have executed by now so you'll see this printed out. You'll say I'm a job X, Y is and in this case three plus five it should show eight there. But then when we print out here X of the job is what? Well what should JX be here? You might think that after the job completes that it should be eight because we added three and five and assigned that to X but it turns out that when a job executes it is working with a copy of the struct not the value which you set up in the main thread. And so when we're mutating X here within the job that's mutating just the job's own copy of the struct and that copy is not gonna be seen outside the job at all not in the main thread not in any other job it's just totally private to the job itself. So in this example what we're actually gonna see is X of the job is three. It's just gonna be this original value as is defined back in the main thread. Now the fact that jobs work with copies of their struct the implication there is that the only way to get data in and out of jobs is through some pointer that is shared between the main thread and jobs. And you would think well okay so I'm gonna just have some reference to an object you know it's just some class instance and then I mutate the class instance and then the change is visible outside the job. And it turns out no we can't do that for a few reasons. One reason is that the worker threads are not ordinary C sharp threads they're native threads and you really should not be accessing anything that's managed code any kind of managed objects things known to the garbage collector. All sorts of bad things can happen if you do try and look with managed objects. So we can't touch managed memory at all we can't in fact touch any class instances where any kind of reference type is verboten within our jobs. We can only work with what C sharp calls blittable types which is their term for a piece of data whose memory representation is the same both in native code and managed code so that to move back and forth between those two worlds you just do a straight mem copy of the bytes and you don't have to you don't have to fix up references or anything like that or worry about garbage collection. That is a blittable type and so the fields of your jobs have to be blittable types and it turns out well simple types like int that's a blittable type float of course and a few other things instruct values as long as their fields are also blittable then it's a blittable type but reference types meaning all class instances or C sharp arrays all those things they are not blittable types they're not allowed in your jobs. So then how do we get data in and out of a jobs if we can't have just like references to data we're working with a copy of the struct we need to have like a pointer to something visible outside the job that we need to then mutate the data through that pointer well that's one reason why Unity provides these things called native containers they're sets of basic container types collection types like arrays, queues, hash maps, lists and a few others those are the main examples but they're implemented as struct types which have unsafe pointers into native memory not garbage collected memory not to managed memory and so there are things where say like a native array you create a native array there's a native memory allocation and that native array struct value has a pointer into that natively allocated memory and because it is native memory you are then responsible for disposing of that memory for deallocating it there's a dispose method you call on your native containers and failing to do so will be a memory leak effectively so you wanna make sure to dispose of your native containers when you're done with them but because these native containers have pointers into native allocated memory if our job includes a field which is a native container type well when the job struct is copied to the thread where it's gonna execute that copy is going to have the same pointer to the same memory so within the job if you then mutate the contents of the native container those chains will be visible once the job is finished and it'll be visible in the main thread and inside other jobs too so that in fact is the only way we get data out of a job jobs should not be doing IO stuff they actually should not be touching static stuff either I think debug.log is one of the few exceptions where it's okay but in general you should not be touching statics you shouldn't be touching static methods or static fields because if they represent managed memory then bad things can happen with the garbage collection and other things so don't do that and really the only way you get data out of your jobs again is through native containers now actually and this will be significant when we talk about ECS the native container types are not technically blittable they're not actually blittable but there's a special exception for them so you can have native container fields in your jobs otherwise what's gonna happen if I have a non-blittle type like say just string here let's just call it S and now let's say just S equals whatever, hi we will get an exception when we call schedule because it's gonna look at this it doesn't mean inspection when you schedule a job and it looks for any fields that are either not blittable or not native containers and you get an exception if it finds any so now say in this example if we did wanna produce something from this job and that is the purpose of jobs to produce new data or to mutate data well then we would need to get it out through some native container and then if we just want a single value the obvious choice is a native array of length one and so here I'm gonna make a field which is a native array of length one let's see it'll be an int and we'll just call it R and then down here we're gonna create R equals a new native array and when you create a new native array the constructor is expecting its length here just one and you also have to specify which memory allocator to use there are three options there's this enum here and we wanna let's just say temp job yeah there we go there's also temp and persistent using the temp job allocator means that this native array should not live for more than four frames there are safety checks that are executed with our native containers within the editor not in production builds but within the editor that will look at our native containers and detect when they've lived for longer than four frames and you'll get an exception I don't know why exactly four frames I think they decided well commonly you need things that live like one frame or two frames and maybe three or four frames but beyond that you shouldn't be using a temp job it's for very short live things so temp job is meant for allocations which need to live for one, two or three frames basically there's also the persistent allocator which is for just indefinite lifetimes things that may live for many frames and there's no time schedule to when they need to be deallocated there's no safety checks to make sure hey you must have disposed of this by now but the persistent allocator is basically just a wrapper for malloc so it's less efficient temp job uses a more efficient strategy and then the third allocator is called temp not temp job just temp and that's a faster allocator but it's for things that only live within that single frame and so generally when you use temp they're for things which you will immediately dispose of within that same method anyway so in this example having created a native array we need to make sure it gets disposed of when it's no longer needed so I'll do so here after the job completes let's see it's nodes j.r and then dispose is the method okay so that's us getting rid of the array and then here in the job what we're gonna do is we're gonna store in that single slot of the array just has a length of one so just index zero we're gonna store the sum we can still print out x that's fine but anyway if the job stores the result in the single slot of this array then after the job completes we know for sure that if we print out j.r sub zero this should be the sum of x and y which should be eight and I've actually forgotten one thing here I need to add using dot unity collections to have access to native array in the allocator okay so that should be good now let's also change this from printing x I should print out the sum which is stored in the array and this text is not really accurate I'm gonna say r sub zero of the job is there okay so now we should see I'm the job x plus y is this should be eight and then after the job completes this should also say eight so let's play the game and it says yep I'm a job x plus y is eight and r sub zero of the job is eight so we computed something in the job and then smuggled it out via a native container now what I've done is I've made the example a little bit more typical of how jobs are used very commonly we want to initiate our jobs schedule them at the start of a frame and then complete them at the end of the frame so now I'm scheduling the job in update and then completing it in late update and because now in these two separate methods we need to access the same job handle and the same array to dispose of it I've made those fields so the array has been assigned to this r field and the job handle is this field called h and of course it's now being performed every frame so if we go play the game you'll see it print out every frame now let's make things interesting by scheduling two jobs instead of one every frame and we're just creating another instance of the same test job type so it's gonna be running the same code but it has its own separate struct so here j is still the values x y in the same array y as k is the value 11 for x six for y but it's array field is the very same array which is gonna be a problem because when we schedule them both at the same time such that they can run in parallel yes each job is working with its own copy of the native array struct but both of those structs have pointers to the same piece of native allocated memory and so now we have in effect two jobs that concurrently are accessing the same memory which of course is generally not desirable generally we want to coordinate access to mutable shared state what might happen in the setup is maybe j will finish executing before k ever starts or vice versa maybe k finishes before j starts or they might overlap partially or in whole so their execution of their code could be interleaved running on simultaneously on separate cores and this is bad first off just because it's indeterminate and we generally care about controlling the outcomes we get of course and of course in many other cases where you have jobs touching the same memory and mutating the same shared state we could get totally incorrect results we could have bugs right so we want to avoid these scenarios we wanna detect them and happily what the job system does is in coordination with the native containers there are some safety checks when k is scheduled here and is called a schedule it's gonna look and say well are there any other jobs that are currently scheduled that also are accessing the same native container that this job wants to access and if so you're gonna get an exception our update method is gonna bail out at this point and scheduled batch jobs will never be called but that won't actually stop the j job from running because down here when we complete when you complete a job if it hasn't already been ready for execution complete will ready the job and then execute it in return but our second job k will never run here we'll just see exception so let's just see what we get in the console hit play and we're getting high on the job x plus y is eight that's the j job but then for the k job we're just getting these exceptions when we call schedule on k so what do we do about these scenarios we have two jobs that are conflicting in the native containers they wanna access they should not be scheduled in an overlapping fashion we wanna make it explicit and deterministic which is gonna finish before the other starts we need to decide here it's up to us to decide whether j should run before k or vice versa but whichever we choose we need to make sure that is guaranteed rather than left up to happenstance of the scheduling so there's two ways to do this the first way is that we can just complete j before we ever scheduled k so right here for just add complete the second called complete over here we could just remove it of course but it's actually harmless after the initial call to complete on a scheduled job any subsequent calls to complete or just they do nothing they're harmless but anyway now we've guaranteed that before k is scheduled j will have completed executing and so when k is scheduled now there aren't any currently scheduled jobs that are trying to access the same native array and we won't get an exception everything will be cool let's see this in action hit play and no exceptions and every j job is printing out x plus y is eight and every k job is printing out x plus y is 17 so that's one way to do it we can just complete a job before scheduling other jobs that want to touch the same data but the other thing we can do and this is the core cool feature of the job system is that when we schedule a job we can give it a dependency we can say this job is dependent on this other job such that when it's executed it will not start it will not be pulled after that queue of jobs until the one it depends on has finished and the way we create this dependency is very simple just when we schedule we pass in the handle of the dependency so now when we schedule k we're saying that it's dependent upon j and so k will never start executing until the j job it depends on has finished and we'll get the same effect here now if we come back and play we should just see the same thing printed out yeah eight 17, eight 17, eight 17 now you can have whole chains of dependencies we can have a which is a dependency of b which is a dependency of c which is a dependency of d you can have it long chains like that if you want sometimes that's what you want and that would mean that a would have to finish before b would start and b would have to finish before c would start, et cetera but another thing we can do is you can have a single job which is a dependency of multiple other jobs you can have say a which is a dependency of not just b but also c and d such that b, c and d will not start executing until a has finished but assuming b, c and d are not dependencies of each other then they can all run concurrently they can run in parallel and that's fine again assuming that they do independent things if they're not touching the same mutable state we want to set up these dependencies so that when we have conflicting jobs when they're touching the same data we need to figure out well logically for our purposes which one needs to be done first and having made that choice we then need to enshrine it in the chain of dependencies and then we can schedule the jobs and kick off their execution and also understand here having made j a dependency of k I don't need to complete j anymore I can just complete k because when you complete a job that is implicitly completing the jobs that depends upon first so when I call complete here on h2 we're completing j first and then k and so again in the scenario where say a is a dependency of b, c and d but b, c and d are not dependencies of each other well if they're the tail end of your dependency chain there's nothing which they are dependency of yes you would have to call complete on b, c and d directly but no matter which one you call complete on a would be completed first and lastly one more thing we can do is we can have a single job depend upon multiple other jobs directly so instead of having a linear chain we could have say that a, b and c are all directly dependencies of fourth job d and so d would not start executing until a, b and c have all finished executing though they respective to each other can run in parallel they don't have to wait for each other but only once they've all finished will d start executing that's what that would mean and the way we achieve that is there's a method called it's a jobhandle.combineddependencies there we go and it has three overloads one takes two jobhandles and you get back this other jobhandle which I'll call combined so let's just see if I have handles a and b here I don't actually, but if I did there's also an overload that takes three so you combine three into one or you can pass in a native array of jobhandles if you wanted to combine more than three at once and so anyway so however many we pass in whatever handles we pass in they can get combined into one logical handle and now having got this combined handle what I could do is I'm scheduling some other job let's just say some other job here and I schedule it and I pass in combined as the handle and then this other job would directly depend upon everything combined together into this one jobhandle so because you can have a single job be a dependency directly of multiple other jobs and also because you can combine jobs effectively what our chain of dependencies looks like is not necessarily linear you can have like branches in it but it does have to be a cyclic you can't have a depend upon b which depends upon c and then have c depend upon a because that would create a cycle so that's dependencies and let me be clear let me emphasize what our jobs do is they don't do IO work they just do in-memory data transformations and the only kinds of things they can mutate are these native containers and so if you have two jobs which aren't touching the same native containers there's really no reason you would make one a dependency of the other that would be a lost opportunity for parallelization you want to be able to as much as possible run things concurrently it's to maximize utilization of the course so when you set up these dependencies it's really to work out a guaranteed execution order for the things which do conflict for the jobs which are touching the same native containers I should be clear here this is a very silly totally artificial example that doesn't really actually make any sense because in the first job we're mutating this array the single slot of this array which is then clobbered in the second job so effectively we're never utilizing the results of the first job so it's a very arbitrary example but it does demonstrate the rules of the system so again within the editor when you play your game there are safety checks performed when you schedule jobs to make sure that there's no conflict within the already scheduled job in how it accesses any native container and if there is a conflict you will get an error in the console but there's also a safety check performed any time you access a native container in the main thread to check if it's in use by any currently scheduled job so here if I just after scheduling this single job J if I then try and print out the contents of the array this is going to trigger an exception because we currently have J scheduled using that same array and it hasn't yet completed and so now if I play this game you'll see the job is still executing we still see it printing out this message but that's because we've already scheduled the job but then once we try and access the array after scheduling we get an exception and it's telling us that we're trying to access this array which is in use by a scheduled job so what we need to do is we need to make sure the job is completed on the main thread and then we can access the job then this access is okay here come back and play the game and we're good now as you might imagine these safety checks are quite expensive because they happen every single time on the main thread that we access a native container well that's why these are only enabled in the editor they're not enabled within production so if we were to come here and actually build my game and play the build version these safety checks would not be performed now in some cases you may wish to also disable these checks within the editor because maybe they're just too costly so you can do so under jobs there are various options for disabling certain safety checks leak detection for example is the safety check for making sure that you've disposed of a native container within its intended life cycle so most commonly what we do in jobs is we have some collection to iterate through some array usually and so we're gonna go from indexes zero up to some number n and often then what would be nice to be able to do is to split that loop of iterating from zero to some number n into sub ranges where those sub ranges can be processed automatically in their own separate jobs so I wanna find just a single job and I just have a single job handle but then behind the scenes it's split up into multiple actual jobs for me so they can be scheduled in parallel and maximize utilization of the course that's why we have I job parallel four which is like I job but the execute method takes in an index and so this represents just the work for one iteration of that logical loop but then when we schedule the job down here we specify first some number of iterations to perform and then we specify an advisory batch size so this first number determines the total number of calls to execute that will be performed in this job but the second number is advising the job system if you're gonna split this up into sub ranges how large should those sub ranges be but this is just an advisory number so the job system in some cases may decide otherwise anyway so concretely here in this example this job has an array and each time through their array we're taking the value at each index and multiplying it by 100 and then storing that back in that index of the array and the update here we create the array given it a length of 10 let me make that bigger so the example make more sense there 100 that's better and then we're iterating through the array and assigning at each index the number which is the index itself so index zero will have value of zero index one will have value one, et cetera anyway so we're setting up the job scheduling it specifying that we need to iterate 100 times so execute will be called in this job 100 times first with index zero then one, then two, then three, then four, et cetera all the way up to and through 99 and we're specifying a batch size of 32 so most likely what the job system will do is then have one actual job which covers indexes zero through 31 another that covers indexes 32 through 63 and a third that covers 64 through 95 and then you get that one last job which of course is a remainder so it has to be just four indexes not 32 and so it'll just do indexes let's see 96 through 99 so we'll probably in this case get four actual jobs that get scheduled and they can all run in parallel but of course only once they're all completed will this logical job, the thing I have the handle to here only then will it be considered finished and if you're worried that this sounds like a lot of method call overhead because we're calling execute 100 times yes but almost certainly the compiler will inline the calls to execute so each batch has a loop going over some sub range and in that you have the call to execute but almost certainly the compiler will inline that call so I think in practice you probably won't ever pay really the cost of all these separate execute calls anyway so we have our job scheduled we have the single handle to it we kick off execution of all jobs on the queue and then in our late update we're gonna complete that job which of course is gonna wait for however many jobs this gets split up into whether it's four actual jobs or some other number they will all have to finish before the job will be considered complete and then once it's done we can print out here the value of in this case let's see our subscript five that would be five times 100 so it should be 500 we should see the value of 500 printed out and so let's see that in action here so let's see if we get 500 play the game and yep it's printing out 500 every frame value of r5 is 500 so that's our job parallel four you're probably wondering how big should my batch sizes be well if your jobs do very little work good typical size is something like 32 or 64 nice powers of two and then if your job each execute does a really significant amount of work very often you may just wanna specify batch size of one the trade off here is that the smaller the batch size the more overhead in terms of the number of actual jobs that get managed and all the associated scheduling overhead there but on the other hand if they're doing each a substantial amount of work you wanna maximize parallelization of all these different jobs and that will tend to lead to the maximal core utilization on the other hand if you're doing very little work per job like in this case you generally want a larger batch size 32 or 64 or something like that because in those cases it's probably not worth it really to pay the overhead of having a bunch of actual jobs to manage within the scheduler