Threads and async
A world that does heavy work — generating a mesh, pathfinding, simulating, parsing a downloaded file — can spread it over several threads instead of blocking the frame. FL# gives you the real System.Threading and System.Threading.Tasks: Thread, the thread pool, Task.Run, Parallel, lock, Monitor, SemaphoreSlim, and the concurrent collections. The threads share the world's memory, so they see the same objects and arrays.
There is one rule: the API that touches the world — objects, components, players, physics, the network — runs on the main thread only. A call to it from another thread throws a clear error. Compute on the threads, then come back to the main thread to change the world.
Run work off the frame
using System.Threading.Tasks;
using NodeWorld.Flux;
using Flux;
public class Terrain : FluxScript
{
protected override async void Start()
{
// Build the heightmap on the thread pool: the frame keeps running.
float[] heights = await Task.Run(() => Generate(256));
// Back on the main thread here: the world API is allowed again.
var mesh = new Mesh();
mesh.SetVertices(ToVertices(heights));
GetComponent<MeshFilter>().sharedMesh = mesh;
}
// Runs on a thread of the pool: maths and arrays only, no world API.
private static float[] Generate(int size)
{
var heights = new float[size * size];
for (var i = 0; i < heights.Length; i++)
heights[i] = Mathf.Sin(i % size * 0.1f) * Mathf.Cos(i / size * 0.1f);
return heights;
}
private static Vector3[] ToVertices(float[] heights)
{
var size = (int)Mathf.Sqrt(heights.Length);
var vertices = new Vector3[heights.Length];
for (var i = 0; i < heights.Length; i++)
vertices[i] = new Vector3(i % size, heights[i], i / size);
return vertices;
}
}After await, the rest of the method runs on the main thread, where the world API works. Code before the await and the body of Task.Run run off it: do not touch the world there.
Many threads
Parallel.For and Parallel.ForEach split a loop over the processors; the calling frame waits for them. Thread starts a thread of your own. ThreadPool.QueueUserWorkItem and Task.Run queue work on the pool (one thread per processor but one).
var result = new float[grid.Length];
Parallel.For(0, grid.Length, i => result[i] = Simulate(grid[i])); // uses every core
var worker = new System.Threading.Thread(() => CrunchForever()) { IsBackground = true };
worker.Start(); // stops with the worldThere is no limit on the number of threads; only the device can refuse one, and then the start throws. Every thread of a world stops when the world unloads, background or not.
Share data safely
The threads share the memory, so protect what several of them read and write:
lock,Monitor,System.Threading.Lock,SemaphoreSlim,ManualResetEventSlim,CountdownEvent,BarrierandReaderWriterLockSlimbehave as in .NET.InterlockedandVolatileare the atomic operations of the hardware.System.Collections.Concurrent—ConcurrentQueue,ConcurrentStack,ConcurrentBag,ConcurrentDictionary,BlockingCollection— is the simplest way to pass items between threads.
private readonly System.Collections.Concurrent.ConcurrentQueue<Result> _done = new();
// On a worker:
_done.Enqueue(new Result(i, value));
// On the main thread (Update), apply what is ready:
while (_done.TryDequeue(out var r))
_objects[r.Index].transform.position = r.Position;Come back to the main thread
When a thread has a result to apply to the world, hand it back with MainThread:
using Flux;
await Task.Run(() =>
{
var value = Compute();
MainThread.Post(() => label.text = value.ToString()); // runs on the main thread, next frame
});
// Or, inside an async method on a worker:
await MainThread.Yield(); // the rest runs on the main thread
label.text = "done"; // allowed nowMainThread.Post(action) queues the action for the main thread. await MainThread.Yield() moves the rest of an async method to it. MainThread.IsCurrent tells you where you are.
What runs on any thread
Maths (Mathf, Vector3, Matrix4x4…), your own data, the arrays of a runtime mesh (see Meshes made by scripts) and of an object batch can be read and written from any thread: fill them on your workers, hand them over on the main thread.
Watchdog
The main thread must finish its frame: a call into your scripts from the game (Update, an event, a coroutine step) in which your code runs for more than 10 seconds is stopped (the game's own work does not count), and the world stays shown with a notice. Your compute threads are not watched — they can run as long as they need — but if the main thread waits for one of them (a lock a stuck thread holds, Task.Wait), that wait counts against the 10 seconds. Use await, never a blocking wait, on the main thread.
On the server
The server runs the same threads in its world scripts. It has no main-thread frame budget of its own; the host sets the watchdog (10 seconds by default).
