Everywhere I check, I’m reading about how non-job system Unity isn’t threadsafe and how the entire game runs on one main thread. I’m utterly baffled by how this works. There are a ton of big games (‘big’ as in big scenes with lots going on) that predate the C# job system and ECS such as Subnautica or 7 Days to Die. I’d figure if these games ran off only one thread, the game play would be cripplingly slow or the player’s CPU would be in full swing.
I’m not saying they do or don’t. Maybe I’m underestimating the power of a single-threaded game. I used ILSpy to take a peek in these games to get an idea how they accomplished this, but nowhere do I see anything about threading. I mean, sure, it’s likely they’re practicing good performance techniques, but for games of those sizes, I have my doubts that that alone is allowing good performance on one thread.
However, I don’t know anything on how I’d squeeze out performance like that in my own games. I assume even as of Unity 2020.3, async-await is still a no-no?
It’s entirely possible they are completely single threaded.
It’s also entirely possible they do some processing on other threads. There are serious limitations to how you can directly access Unity objects from other threads, but the entire System.Threading and System.Concurrent namespaces are available for use.
The other thing to consider is that besides CPU multithreading, there are other ways to squeeze extra performance out of your hardware in Unity. Just a couple examples:
Because some game are open world, doesn’t mean it requires multi threading.
There is indeed tons of way to optimize things, before threading is any sensible use for.
Yet even many large (1k + units) scale RTS games, are just single threaded. And that mostly because of determinism challanges.
WoW is also single threaded, while world is massive. Maybe they ofloaded some part of code by now. But there isn’t that much happening. Unless having many characters and NPC in big cities, in small area. But usually that GPU bound.
While most of games run single threaded, ECS is nothing new, neither threading is.
CitySkyline I think uses Entitias, which was made and sold on AssetStore long before Unity ECS, was even available.
Subnautica possibly also using Entitias.
There are lots of people complaining about 7 Days to Die’s performance ;).
And the action is only centered around the player. Often you can see zombies/animals/buildings beeing spawned. So not the whole world is simulated but only the small part the player can see. The terrain meshes are generated procedurally on demand. So when you travel through the world it is large, but only a small part of it is “active” and simulated at any given time. Even in the ECS Megacitiy demo only a part of the buildings is active/loaded. So don’t let them “fool” you about the processing power such games need. Those games are just good at omitting “unecessary” work.
And almost everything about computer graphics in general, and ESPECIALLY realtime interactive graphics is:
- how to cleverly not do the work
That’s literally the secret. It is closely related to the concept of a Potemkin Village, a thin veneer or facade designed to give the impression of a robust and functional larger world:
Hey, Learning lot from this thread, but I want to thank you for this reply. Particularly “how to cleverly not do the work” is really the thing to remember when entangled with complex code. Potemkin village is really the perfect analogy.
For those coming from Google (this is currently the #2 result for “unity multithreading”)
Unity introduced async/await support in version 2023.1 with the Awaitable class that is meant to stand in for Task.
Previously, Tasks were already usable but one had to pay attention to some details such as pending tasks not being stopped automatically when exiting play mode in the editor.
It’s unclear to me how necessary it is to use Awaitable over Task and whether the previous issues with Task were fixed.
The main catch in multithreading with Unity is that a lot (if not most) of the engine’s classes can only be accessed from the main thread.
From what I can tell, the main benefit from the introduction of Awaitable is that coroutine syntax is no longer needed for things such as waiting for the next frame. Previously, one would have to implement a sort of shim with async code interfaced with coroutines.
This example in the documentation does lead me to believe that .NET Tasks are supported:
This is not the case - Unity supports multithreading and has for a long time, even before the introduction of Awaitable.
But I see how one can think otherwise - and I missed this in my previous comment. Awaitable has a somewhat clunky way of running code outside the main thread.
One has to do await Awaitable.BackgroundThreadAsync(); to resume execution outside the main thread:
With the .NET Task class, Task.Run() will do the same in a more semantic way, so that’s what I’ll continue to use.
There is also an “Async, tasks and Awaitable” section in the “Overview of .NET in Unity” documentation page, which previously was “Limitations of async and await tasks” and before that was “Avoid using async and await”.
This page says:
Unity doesn’t automatically stop code runnining in the background when you exit Play mode. To cancel a background operation on exiting playmode, use Application.exitCancellationToken.
So I’ll continue having to use my SafeTask implementation.
Right - in other words you have to go out of your way to get things to run on a different thread, and this still leaves you with all the normal limitations of multithreading. It’s basically just a quality of life improvement vs using System.Threading.
Almost universally when I see people (EDIT: people in this forum using Unity3D) reaching for multithreading or multitasking, this is what happened:
I have a game idea
I tried a simple brute force implementation
oh my gosh it is unbelievably slow on mobile
go to the internet and google
read “multithreading can make programs faster!”
Now people go to their Unity project with this one giant multitasking hammer they found and start smashing everything flat with it, making everything multithreaded and multitasked and doing all this crazy complicated work, obfuscating the original flow of their program, almost always introducing new bugs, new edge cases and complicated use requirements.
Finally they test it on the actual hardware. It didn’t get any faster.
But their tech debt sure went up! What used to be simple in their project (“spawn a creature!”) has become a multithreaded process of firing up a task, creating, configuring and injecting a creature specification object, sending it off to run on the creature-making thread, then hooking it back in using some kind of delegate marshaling scheme… and…
The game is still slow.
Well, turns out that the game was originally slow because they were pushing too many pixels, not because they were running everything on the main thread.
Again, I stand by what I said above:
ALSO:
For all performance and optimization issues, ALWAYS start by using the Profiler window:
Window → Analysis → Profiler
DO NOT OPTIMIZE “JUST BECAUSE…” If you don’t have a problem, DO NOT OPTIMIZE!
If you DO have a problem, there is only ONE way to find out: measuring with the profiler.
Failure to use the profiler first means you’re just guessing, making a mess of your code for no good reason.
Not only that but performance on platform A will likely be completely different than platform B. Test on the platform(s) that you care about, and test to the extent that it is worth your effort, and no more.
Remember that you are gathering information at this stage. You cannot FIX until you FIND.
Remember that optimized code is ALWAYS harder to work with and more brittle, making subsequent feature development difficult or impossible, or incurring massive technical debt on future development.
Don’t forget about the Frame Debugger window either, available right near the Profiler in the menu system.
At a minimum you want to clearly understand what performance issues you are having:
running too slowly?
loading too slowly?
using too much runtime memory?
final bundle too large?
too much network traffic?
something else?
If you are unable to engage the profiler, then your next solution is gross guessing changes, such as “reimport all textures as 32x32 tiny textures” or “replace some complex 3D objects with cubes/capsules” to try and figure out what is bogging you down.
Each experiment you do may give you intel about what is causing the performance issue that you identified. More importantly let you eliminate candidates for optimization. For instance if you swap out your biggest textures with 32x32 stamps and you STILL have a problem, you may be able to eliminate textures as an issue and move onto something else.
This sort of speculative optimization assumes you’re properly using source control so it takes one click to revert to the way your project was before if there is no improvement, while carefully making notes about what you have tried and more importantly what results it has had.
“Software does not run in a magic fairy aether powered by the fevered dreams of CS PhDs.” - Mike Acton
Well, calling Task.Run() is a similar effort - you’ll always have to explicitly specify you want to run something outside the main thread, no?
I do agree Awaitable is mostly a modernization of the whole Coroutine API, and doesn’t seem to fundamentally change the way Unity handles multithreading (as evidenced by the fact that pending tasks still continue running after exiting play mode).
Unity’s SynchronizationContext (linked to from one of the doc pages I linked to previously) seems to have received only a small update upon the introduction of Awaitable.
Edit: I noticed that Awaitable.BackgroundThreadAsync() is essentially a wrapper aroundTask.Run();, so the two shouldn’t be very different in behavior.
It’s not a reaction. It’s an observation from reading a great deal of posts here asking the same questions over and over again about multithreading and improving the performance of a naive implementation.
Of course it is! I never said otherwise. Go read my post again.
My post is narrowly framed to all the Unity3D users posting here having some flavor of multi-whatever problem because they were trying to do it to address a performance issue in Unity.
In fact, in over a decade here I have not seen a single attempt at multi-threading succeed in improving performance.
If you are in this forum asking questions about using multithreading to make your game run faster (the original poster three years ago used the words:
So clearly, they’re already in the cohort of ALL the other reaching-for-multi-threading-for-performance posters on this forum that I’ve seen.
Thus I am surfacing my basic observation again about this problem space.
The most notable thing about Unity’s Awaitable is that it is pooled, so it shouldn’t allocate on every single await. It’s a theory, I didn’t get around to actually test them against each other with Task. (In exchange you shouldn’t await on it more than once.)
Are we counting running the code in a separate thread to process in a single-threaded manner? One of my past projects had a rope simulation handled in that fashion.
That sounds neat, a little custom physics engine running in another thread. That’s a PERFECT use of multi-threading.
From one engineer to another I look at that, nod knowingly at you and give you this big “Hey cool setup bro!” look and you and I know we both appreciated your work in that.
But also… from one engineer to another, we know the vast majority of posters in here asking why they’re having multi-tasking woes are not people in any danger of implementing their own physics system.
But also… just because not everyone is implementing physics on separate threads, that doesn’t mean we should run around and tell everyone how multithreading is bad, IMHO. I think the best approach if we tell them that they need adequate workload to worth something putting on a separate thread and those things should not rely on extensive logic interfacing Unity-data extensively.