Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice
  3. Dismiss Notice

Question Jobs vs Tasks vs Threading vs Co-Routines for sports simulation game

Discussion in 'Scripting' started by dvmartin27, May 24, 2024.

  1. dvmartin27

    dvmartin27

    Joined:
    May 7, 2024
    Posts:
    7
    Hello,

    I am writing a sports simulation game. It simulates an entire league (or indeed multiple leagues, like football manager). The user will only play 1 game per week, which means that the rest of the games have to be simulated. The simulation is a very large piece of code that will typically take ~ 1-2 minutes to simulate a given game.

    The idea is that the simulation should occur seamlessly in the background whilst the user is playing their own game or checking out the user interface (e.g. setting tactics, looking at stats, scouting opponents etc).

    Right now everything happens on the main thread and this means that when multiple games are being simulated at once it really slows down the GUI. I'd like to push all of these simulations of other games onto other CPU threads.

    I've been reading a lot into jobs and such but they seem to be more for things that occur on a short timeframe, like 1 frame or a few frames, and not big simulations that take minutes. Alternatively there are tasks and threading but the documentation for that seems sparse and Unity seems to not be supporting that as much now that Jobs have been officially released over the past couple of years?

    Basically I am happy for any advice here on the best way to multi-thread these long (1-2 minute) simulations.

    Thank you in advance!
     
  2. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    21,686
    Correct. With normal threads you create a thread and have it handle the entire pipeline, but with the JobSystem you break up your pipeline into multiple sections and jobify each of those sections with dependencies handling their execution order. That's because normal threads are fairly heavy to create and destroy while jobs are not.

    Unity's APIs are for the most part not thread safe. You can use multiple threads to do additional computations but once you need to pass the data to Unity it has to be done on the main thread. This applies to the JobSystem too.

    Finally multithreading isn't a magic bullet. If the logic is very interconnected in its dependencies you're just as likely to see a speed decrease from multithreading due to the logic having to wait on other parts of the logic to complete.

    Systems that see very high increases like physics do so because the calculations they're doing are independent of each other (eg an object in one part of the world doesn't need to know about another object it will never come into contact with).

    That said the JobSystem can also be used in single threaded mode. That might sound redundant but between the Burst compiler and native collections you can potentially end up with a performance increase over normal single threaded code.
     
    Last edited: May 24, 2024
    Spy-Master likes this.
  3. dvmartin27

    dvmartin27

    Joined:
    May 7, 2024
    Posts:
    7
    Thank you very much @Ryiah for your response.

    I believe that I can program it in a way that is not very interconnected. Actually, it is already programmed like this, just running on the main thread. I currently setup each game to simulate by passing in basic parameters like the team tactics, player attributes etc, and then each game gets simulated. It's slow, because it's all on 1 thread. When each game is finished it checks to see if the others have too. Only once all games have been finished will the outputs be sent back into the main game world, e.g. to update player season stats, team win-loss records etc.

    Since the simulation would take a couple of minutes, the heavy overhead of creating and destroying threads would not be an issue?

    I appreciate that the Unity API's are not thread safe. This will require a bit of re-programming but nothing that is currently beyond me.

    So it seems like I should ignore jobs and just use threads. However, what about 'tasks' and 'coroutines'. This is where I get confused with the Unity lingo...
     
  4. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    21,686
    Agreed, I don't think it should be an issue at all, and I definitely would start with threads/tasks over jobs as they're very easy to get started with. If everything is already built to be run independent you can just spawn one and pass the starting method to it.
     
    dvmartin27 and Spy-Master like this.
  5. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,594
    Jobs - break up a workload into multiple smaller units of work that then get distributed over multiple threads and operated concurrently. Think how if you have 1000 positions that need to be updated and you have 8 CPU cores available, so you divide it into 125 position updates across 8 threads to facilitate maximizing the CPU usage.

    Tasks - facilitates asynchronous work flows. This does not mean that the work is therefore threaded, though threading may get involved. An example of a task that isn't threaded is say awaiting for a web request to return. By using a task you can stop executing your logic and then return executing your logic when the web request has come back from the server. In the mean time your thread can continue on doing other work. Mind you the thing that you await COULD be another thread though. You could spin up a task on a separate thread (the task library facilitates this via its ThreadPool) and then await that tasks workload... but really this is all just some syntax sugar and api magic wrapped around the existing threading library allowing you to await inline rather than using things like callback delegates, waithandles, and the ilk.

    Threading - the fundamental aspect of the C#/.Net library that facilitates spinning up threads within the .Net runtime (I make this distinction since unity can spin up threads independent of the .net framework since the engine is not exclusively .net). When the task library does create a thread, it's using the threading library. You can do so via something like the Task.Run method:
    https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.run?view=net-8.0

    In the vast majority of scenarios you likely shouldn't be using the threading library directly and instead relying on the task library to spin up threads via its ThreadPool. Or using an alternative system like jobs when applicable.

    Coroutines - coroutines are a hacky unity specific thing that exists from a time before the async/tasks library being available to Unity (tasks didn't exist when Unity was first created, and even after tasks was added to C#/.net, unity still didn't have it because it was relegated to a very old version of .net for quite some time until the likes of IL2CPP came about). They effectively do something similar to the async/tasks library by exploiting the "iterator function" feature of C#. It still exists to this day because of its legacy status. That vast majority of stuff you could do with Coroutines could be done with a Task.

    ...

    Which one should you use?

    Well Coroutines and Threading is sort of tossed out the window IMO since Tasks is better suited to either of them. Tasks directly facilitates threading via its threadpool if needed. And Tasks effectively do very nearly everything, if not everything, that a coroutine can; if only requiring a slightly different syntax and/or logical lift.

    So in the end the question is do you use Jobs or Tasks (w/ threading).

    And I'd say this...

    Are you trying to speed it up, or are you trying to just avoid making the main thread/UI hang?

    If you just want to avoid having the main thread hang. Go with a Task. If your simulation doesn't access the unity api (which isn't thread safe) you can just call Task.Run and do all your work on another thread. Await that task to know when it's done. If the work does require accessing the unity api though (the simulation also renders) a task is still useful... you'll just have to NOT us a thread on Task.Run and instead await a Task.Yield on some interval and make sure you only increment your workload of the simulation a little bit per frame (that or remove dependencies on the unity api by tokenizing your simulation in a manner that doesn't rely on the unity api).

    If you want to try to optimize it by exploiting all of the cores of your CPU... jobs might be useful as long as it's non-trivial to jobify your workload. Knowing if your workload is easily jobifiable though requires more information of what your workload actually entails. But if your simulation is just 1 long sequential operation (i.e. can't be distributed concurrently), jobs won't really benefit you much and you can just stick with a task.
     
    Last edited: May 25, 2024
    Ryiah likes this.
  6. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    6,922
    Bursted jobs.

    Given the right optimizations, specifically vectorizable code, I imagine those 1-2 minutes could even go down to 1-2 seconds instead.

    If you use Tasks, you forfeit all Burst compiler optimizations and may end up with multithreaded code that may end up being only slightly faster than before, like "just" 50% faster. But if your code lends well to multithreading and isn't using Unity APIs then Tasks might offer the route of less friction. However, given how much faster you can make those 1-2 minutes - that's a serious wait time even for a simulation - I would definitely try to use bursted Jobs here.
     
  7. dvmartin27

    dvmartin27

    Joined:
    May 7, 2024
    Posts:
    7
    Thank you very much everyone for your advice!

    It sounds like tasks would be the easier route and perhaps the most logical, with the caveat that @CodeSmile mentioned with respect to the Burst Compiler, which is indeed a big deal (and I have seen this with my brief tests of jobs).

    I believe that the biggest coding work ahead of me is to eliminate all references to the Unity API from the simulation code. It's currently littered with them, because the code was written initially for playing a game in real time (so with game objects that move around and graphics and transforms etc) and then hacked away such that it could be either played live or simulated in the background, but if in the background that basically meant making the game objects invisible.

    So I'm going to go through and do all of this first, since that would be needed for both Tasks and Jobs.

    I'll probably then start with Tasks, since that's the easiest, and see the speed improvement. If it's still lacking and I feel like I really need the Burst Compiler, I'll give that a go, but will have already done a bunch of the necessary work.

    Thank you again everyone! I'll report back (probably in a few months)