Logo Mibo

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 compute (input: 'Input) = computeOnBackgroundThread input
let finished (result: 'Result) = WorkComplete result
let failed (_ex: exn) = WorkFailed input

let heavyWork (input: 'Input) : Cmd<Msg> =
  Cmd.ofAsync
    (async { return compute input })
    finished
    failed

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:

let doneOrFallback (result: 'Result) = WorkDone result
let fallback (_ex: exn) = WorkDone (fallbackResult input)

Cmd.ofAsync
  (async { return compute input })
  doneOrFallback
  fallback

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 problems: request deduplication.

When to use

See also

val compute: input: 'Input -> 'a
val input: 'Input
val finished: result: 'Result -> 'a
val result: 'Result
Multiple items
module Result from Microsoft.FSharp.Core

--------------------
type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
val failed: _ex: exn -> 'a
val _ex: exn
type exn = System.Exception
val heavyWork: input: 'Input -> 'a
val async: AsyncBuilder
type Model = { PendingWork: obj Results: obj }
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
val ignore: value: 'T -> unit

Type something to start searching.