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 For163793 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 Lockarena: 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}")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")[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.