NodeWorld

Threads et asynchrone

Depuis SDK 0.2.1

Un monde qui fait un gros travail — générer un mesh, chercher un chemin, simuler, lire un fichier téléchargé — peut l'étaler sur plusieurs threads au lieu de bloquer l'image. FL# te donne les vrais System.Threading et System.Threading.Tasks : Thread, le pool de threads, Task.Run, Parallel, lock, Monitor, SemaphoreSlim et les collections concurrentes. Les threads partagent la mémoire du monde : ils voient les mêmes objets et les mêmes tableaux.

Il y a une règle : l'API qui touche au monde — objets, composants, joueurs, physique, réseau — ne tourne que sur le thread principal. L'appeler depuis un autre thread lève une erreur claire. Calcule sur les threads, puis reviens sur le thread principal pour changer le monde.

Sortir le travail de l'image

csharp
using System.Threading.Tasks;
using NodeWorld.Flux;
using Flux;

public class Terrain : FluxScript
{
    protected override async void Start()
    {
        // Construit la carte de hauteurs sur le pool : l'image continue.
        float[] heights = await Task.Run(() => Generate(256));

        // De retour sur le thread principal ici : l'API du monde est de nouveau permise.
        var mesh = new Mesh();
        mesh.SetVertices(ToVertices(heights));
        GetComponent<MeshFilter>().sharedMesh = mesh;
    }

    // Tourne sur un thread du pool : des maths et des tableaux seulement, pas d'API du monde.
    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;
    }
}

Après await, la suite de la méthode tourne sur le thread principal, où l'API du monde marche. Le code avant l'await et le corps de Task.Run tournent en dehors : n'y touche pas au monde.

Plusieurs threads

Parallel.For et Parallel.ForEach répartissent une boucle sur les processeurs ; l'image appelante les attend. Thread démarre un thread à toi. ThreadPool.QueueUserWorkItem et Task.Run mettent du travail sur le pool (un thread par processeur moins un).

csharp
var result = new float[grid.Length];
Parallel.For(0, grid.Length, i => result[i] = Simulate(grid[i]));   // utilise tous les cœurs

var worker = new System.Threading.Thread(() => CrunchForever()) { IsBackground = true };
worker.Start();   // s'arrête avec le monde

Le nombre de threads n'est pas limité ; seul l'appareil peut en refuser un, et le démarrage lève alors. Tous les threads d'un monde s'arrêtent au déchargement du monde, en arrière-plan ou non.

Partager des données sans risque

Les threads partagent la mémoire : protège ce que plusieurs lisent et écrivent.

  • lock, Monitor, System.Threading.Lock, SemaphoreSlim, ManualResetEventSlim, CountdownEvent, Barrier et ReaderWriterLockSlim se comportent comme dans .NET.
  • Interlocked et Volatile sont les opérations atomiques du processeur.
  • System.Collections.Concurrent — ConcurrentQueue, ConcurrentStack, ConcurrentBag, ConcurrentDictionary, BlockingCollection — est le moyen le plus simple de passer des éléments d'un thread à l'autre.
csharp
private readonly System.Collections.Concurrent.ConcurrentQueue<Result> _done = new();

// Sur un worker :
_done.Enqueue(new Result(i, value));

// Sur le thread principal (Update), applique ce qui est prêt :
while (_done.TryDequeue(out var r))
    _objects[r.Index].transform.position = r.Position;

Revenir sur le thread principal

Quand un thread a un résultat à appliquer au monde, rends-le avec MainThread :

csharp
using Flux;

await Task.Run(() =>
{
    var value = Compute();
    MainThread.Post(() => label.text = value.ToString());   // tourne sur le thread principal, à l'image suivante
});

// Ou, dans une méthode async sur un worker :
await MainThread.Yield();   // la suite tourne sur le thread principal
label.text = "fini";        // permis maintenant

MainThread.Post(action) met l'action en file pour le thread principal. await MainThread.Yield() y ramène la suite d'une méthode async. MainThread.IsCurrent dit où tu es.

Ce qui tourne sur n'importe quel thread

Les maths (Mathf, Vector3, Matrix4x4…), tes propres données, les tableaux d'un mesh de runtime (voir Meshes faits par les scripts) et d'un lot d'objets se lisent et s'écrivent depuis n'importe quel thread : remplis-les sur tes workers, rends-les sur le thread principal.

Chien de garde

Le thread principal doit finir son image : un appel de tes scripts par le jeu (Update, un événement, une étape de coroutine) dans lequel ton code tourne plus de 10 secondes est arrêté (le travail du jeu lui-même ne compte pas), et le monde reste affiché avec un avis. Tes threads de calcul ne sont pas surveillés — ils tournent aussi longtemps qu'il faut — mais si le thread principal attend l'un d'eux (un lock tenu par un thread coincé, Task.Wait), cette attente compte dans les 10 secondes. Utilise await, jamais une attente bloquante, sur le thread principal.

Sur le serveur

Le serveur fait tourner les mêmes threads dans les scripts du monde. Il n'a pas de budget d'image à lui ; l'hébergeur règle le chien de garde (10 secondes par défaut).