So, you want to use multithreading in Unity WebGL

Jonas Hundertmark
medialesson
Published in
4 min readJan 18, 2022
Photo by Bozhin Karaivanov on Unsplash

First of all, don’t.

At the time of me writing this, WebGL threads support in Unity is amazingly, hilariously unstable. There’s no guarantee your application won’t just randomly crash, freeze up, or choke itself to death while trying to process. The Unity-API is generally not thread safe, so threading can only be used for a limited number of tasks, anyway. If you can at all help it, I would strongly advise you to keep all your work on the main thread and use concurrency instead.

However, if you absolutely need to use parallel threads, for whatever reason, here’s how I would go about it:

Before you start

Threads support broke sometime during Unity 2020.1’s development. If you set webGLThreadsSupport to 1 in your ProjectSettings.asset file, your build won’t even complete on Unity 2020.1.1 or later. Even then, 2020.1.0’s memory management is extremely sketchy on WebGL, so my recommended release would be the latest build of Unity 2019.4 LTS.

You will also need a simple HTTP server that allows you to set custom headers, since multithreading (more specifically: accessing the Shared Array Buffer) requires you to set a few extra CORS headers, which Unity’s Build&Run doesn’t do on its own. If you’re using python’s SimpleHTTPRequestHandler you can quickly set them like this:

handler.send_header(‘Access-Control-Allow-Origin’, ‘*’)
handler.send_header(‘Cross-Origin-Opener-Policy’, ‘same-origin’)
handler.send_header(‘Cross-Origin-Embedder-Policy’, ‘require-corp’)

Lastly, we need to rename Atomics.wake() into Atomics.notify(), or else Chromium-based browsers won’t know how to actually run the WebGL player. Navigate to your Editor’s install directory and then go to

\Editor\Data\PlaybackEngines\WebGLSupport\BuildTools\Emscripten\src\library_pthread.js

Open the file using a text editor of your choice and replace all instances of Atomics.wake with Atomics.notify. It should appear inside the function emscripten_futex_wake.

Should look something like this.

Once all that is done, we can finally take care of the editor itself.

Setting your ProjectSettings via Editor-Script

Manually editing Unity’s ProjectSettings.asset file is cumbersome and Unity likes to overwrite your ProjectSettings all the time, so we use an editor script to set the correct values whenever we start a new build.

Create a new C# file in Assets/Editor, paste the above code, and you’re good to go.

For WebGL EmscriptenArgs, I found -s ALLOW_MEMORY_GROWTH=1 works most reliably, but you can also use -s WASM_MEM_MAX=2032MB or any other value if you’d like to further restrict memory usage (2032 MB is Unity’s upper limit in WebGL).

If you did everything correctly, you should be able to create a multithreading build, which properly loads on your custom HTTP server.

Using Threads in WebAssembly

If you got this far, the good news is that both regular old C# threads and Unity’s own Job System should work fine at this point. Jobs seem to have a slight edge over threads in terms of speed, but you can use whatever you feel most comfortable with.

One weird thing to keep in mind, if you’re running a parallel thread over a long peroid of time and you want to check whether the thread has finished processing: There seems to be a huge difference in terms of stability, when in the game loop you check for thread completition. Both WaitForUpdate() and WaitForEndOf Frame() resulted in random freezes from time to time that I can’t really explain. The most stable option seems to be a simple yield instruction. So yield return null; if you’re in a Coroutine or await Task.Yield(); if you’re in an async function. It might have something to do with the garbage collector… Speaking of:

Managing Memory usage

WebAssembly uses its own Garbage collection routines which Unity can only partially access. Calling GC.Collect() at any point will do exactly nothing. Unity will try to collect a little bit of garbage at the end of every frame, but in general, the only way for you to get a full garbage collection cycle started is to reload your scene. Even then, WebAssembly might randomly decide not to deallocate memory. There were situations where memory allocation persisted, even after a cached reload of the web page. This is, of course, hugely frustrating, and the only solution here seems to be staying very conservative with your RAM requirements and always cleaning up after yourself. Chromium-based browsers will straight up crash once they allocate over 2GB of memory on a single thread. Firefox will - admirably - try to soldier on, no matter how much torture it has to endure. I’ve managed to fill Unity with about 4.2GB of trash on a single thread before WASM’s garbage collector finally did it’s thing. Your milage may vary of course, but the best solution here is to just never allocate that much memory in the first place.

Conclusion

Platform limitations should always be taken into consideration when designing games and apps in Unity. WebGL currently offers no direct GPU access (for things like ComputeShaders etc.), and no real way to have more than one thread running concurrently. The above method should be viewed for what it ultimately is, which is a very messy hack to coax the engine into doing something it really doesn’t want you to do. Multithreading is a fascinating topic, and working on this problem has given me some cool insights into the way Unity works though WebAssembly. In the future we might get a more mature implementation of multithreaded games running through your web browser. But if you’re looking to create a quick Prototype or proof-of-concept for what might be possible then, you can get a glimpse of it today.

--

--