Skip to content
FutureBlitz

    Install

    Guide

    Concurrency and async

    The worker pool, Spawn and Tasks, Parallel For, Channels, Mutex and Atomic, Sendable, and coroutines.

    A FutureBlitz program has a main thread, which runs its top-level code and owns the game, and a worker pool of one thread per core, minus one, for computation. Threads that block for a long time, like a network listener, get a thread of their own with Thread.Start. Coroutines run on the main thread, taking turns at each Await.

    Spawn and Await

    Spawn runs a Function on the pool and gives a Task<T>. Wait() blocks until it’s done and gives its result, or throws its error; Await does the same without blocking, inside an Async Function.

    Import std.core
    
    Function Checksum(seed: Int): Int
        Local h = seed
        For i = 1 To 200_000
            h = (h * 31 + i) % 1_000_003
        End For
        Return h
    End Function
    
    Local tasks: Task<Int>[] = []
    For seed = 1 To 4
        Const s = seed
        tasks.Add(Spawn(Function() => Checksum(s)))
    End For
    
    For t In tasks
        Print(t.Wait())
    End For
    Output
    163793
    422025
    680257
    938489

    A lambda given to another thread can capture Consts, like s here, but not Locals, which could change while it runs.

    Sendable: data races don’t compile

    What another thread’s work uses has to be Sendable: numbers, Strings, Structs and arrays of them, and Shared Classes, whose Fields are ReadOnly, or an Atomic<T>, or behind a Mutex<T>. An ordinary Class isn’t, so passing one to Spawn is a compile error, and so a data race is a compile error.

    Import std.core
    
    Shared Class Scoreboard
        ReadOnly Field title: String
        Field kills: Atomic<Int>
        Field best: Mutex<Int>
    
        New(title: String)
            Self.title = title
        End New
    End Class
    
    Const board = Scoreboard("arena")
    Parallel For i = 0 Until 8
        board.kills.Add(1)
        Lock board.best As top
            If i * 10 > top Then top = i * 10
        End Lock
    End For
    
    Lock board.best As top
        Print("{board.title}: {board.kills.Load()} kills, best score {top}")
    End Lock
    Output
    arena: 8 kills, best score 70

    Channels

    A Channel<T> carries values from one thread to another, in order, holding up to its capacity. For x In channel takes values until it’s closed.

    Import std.core
    
    Function Produce(out: Channel<Int>)
        For i = 1 To 5
            out.Send(i * i)
        End For
        out.Close()
    End Function
    
    Const squares = Channel<Int>(2)
    Local producer = Spawn(Function() => Produce(squares))
    Local total = 0
    For n In squares
        total += n
    End For
    producer.Wait()
    Print("total {total}")
    Output
    total 55

    Coroutines

    An Async Function called without Await starts a coroutine, which runs until its first Await and goes on when what it waits for is done, all on the thread it started on. Wait, NextFrame, and RealWait from std.time wait for time to pass. A Scheduler of the program’s own is ticked by hand, so its coroutines pause when it isn’t ticked, and slow down when its time does.

    Import std.core
    Import std.time
    
    Global log: String[] = []
    
    Async Function Blink(name: String, every: Duration, times: Int)
        For i = 1 To times
            Await Wait(every)
            log.Add(name)
        End For
    End Function
    
    Local game = Scheduler()
    game.Run(Blink("fast", 100.Milliseconds, 4))
    game.Run(Blink("slow", 250.Milliseconds, 2))
    
    Local ticks = 0
    While !game.IsIdle()
        game.Tick(0.05)
        ticks += 1
    End While
    Print(log)
    Print("{ticks} ticks of 50 ms")
    Output
    [fast, fast, slow, fast, fast, slow]
    10 ticks of 50 ms

    An Async Function can Await a Spawn’s Task too, so the heavy part of loading a level runs on the pool while the game keeps drawing frames, and the rest goes on back on the main thread. The engine’s Flip() resumes the main thread’s coroutines each frame; in a program without it, they run once top-level code ends, until none waits.

    @MainThread marks what only the main thread can call, like the engine’s drawing, and calling it from a Spawn is a compile error. Spec chapter 10 has the whole model, and std.concurrent has more: locks, ConcurrentMap, WhenAll, WhenAny, and cancellation.

    Report an issue with this page or with FutureBlitz on GitHub.