Header menu logo Mibo.Raylib

Background Work

What and Why

Games need to do heavy work — generate worlds, pathfind for dozens of enemies, load assets, save game state. Doing this on the main thread drops frames. Doing it naively on background threads creates race conditions, duplicate requests, and crash-prone synchronization.

The pattern: use Elmish commands to run async work on the thread pool, return results as messages, and process them on the main thread. The framework handles the threading — you write the async work, the framework delivers the result.

Use Cases

World generation

Procedural terrain, chunk loading, dungeon generation. Generate chunks around the player as they move, evict distant ones to reclaim memory.

Pathfinding

A* or flow-field calculations for dozens of AI units. Each unit's path computes on a background thread, result arrives as a message, movement system reads it next frame.

Asset loading

Load textures, models, and sounds at runtime without freezing. A loading screen shows progress while assets stream in from disk.

Save/load

Serialize large game states — hundreds of entities, world data, inventory. Write to disk on a background thread, show a "Saving..." indicator.

Procedural content

Loot table rolls, enemy wave composition, event triggers. Compute results off the main thread, apply them when ready.

The Technique

Any Async<'T> can run as a command:

let heavyWork (input: 'Input) : Cmd<Msg> =
  Cmd.ofAsync
    (async { return computeOnBackgroundThread input })
    (fun result -> WorkComplete result)
    (fun _ex -> WorkFailed input)

Track pending work to prevent duplicates:

type Model = {
  PendingWork: HashSet<string>
  Results: ConcurrentDictionary<string, Result>
}

Check the tracking set before dispatching:

if not(model.PendingWork.Contains(key)) then
  model.PendingWork.Add(key) |> ignore
  heavyWork input

Process the result on the main thread:

| WorkComplete result ->
  model.PendingWork.Remove(result.Key) |> ignore
  model.Results[result.Key] <- result
  struct (model, Cmd.none)

Error handling

The error callback provides a fallback — retry synchronously, return a default, or skip:

Cmd.ofAsync
  (async { return compute input })
  (fun result -> WorkDone result)
  (fun _ex -> WorkDone (fallback input))

Key Insight

The Elmish Cmd system is the threading mechanism. You don't create threads, manage locks, or poll for completion. You return a Cmd, and the framework runs it on the thread pool, then delivers the result as a message on the main thread. The tracking set prevents duplicate requests — not thread safety, but request deduplication.

When to use

See also

val heavyWork: input: 'Input -> 'a
val input: 'Input
val async: AsyncBuilder
type Model = { PendingWork: obj Results: obj }
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
Multiple items
module Result from Microsoft.FSharp.Core

--------------------
[<Struct>] type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
val ignore: value: 'T -> unit

Type something to start searching.