Skip to content
FutureBlitz

    Install

    Concurrency, 1 of 8

    Spawn and Task

    Run work on other cores with Spawn, wait for its result with a Task, and give long-running work a Thread of its own.

    What you’ll build

    A program that adds up two long ranges of numbers at the same time, counts the primes below three limits on three cores at once, and runs a piece of work on a thread of its own. It shows how work goes to other threads, how its result comes back, and what the compiler checks on the way.

    Spawn

    Spawn(work) runs a function on the worker pool, which has a thread for each core but one, and gives a Task<T> at once, where T is what the work gives. Wait() gives the result, waiting for the work to finish if it hasn’t.

    Import std.core
    
    Function SumTo(n: Int64): Int64
        Local total: Int64 = 0
        For i = 1 To n
            total += i
        End For
        Return total
    End Function
    
    Local small = Spawn(Function() => SumTo(1_000_000))
    Local large = Spawn(Function() => SumTo(2_000_000))
    Print(small.Wait() + large.Wait())
    Output
    2500001500000

    Both sums run at once, on two cores, while the main thread goes on to the first Wait(). The work is a lambda or a function’s name that takes nothing, or a Sendable Func, a function value made to cross threads. IsDone() says whether a Task has finished, without waiting.

    Many tasks

    A Task is a value like any other, so an array can hold several. Here each limit gets its own task, and the results are read back in order.

    Import std.core
    Import std.collections
    
    Function CountPrimes(limit: Int): Int
        Local count = 0
        For n = 2 To limit
            Local prime = True
            Local d = 2
            While d * d <= n
                If n % d == 0
                    prime = False
                    Break
                End If
                d += 1
            End While
            If prime Then count += 1
        End For
        Return count
    End Function
    
    Const limits = [10_000, 20_000, 40_000]
    Local tasks: Task<Int>[] = []
    For limit In limits
        tasks.Add(Spawn(Function() => CountPrimes(limit)))
    End For
    
    For i = 0 Until tasks.Count
        Print("primes up to {limits[i]}: {tasks[i].Wait()}")
    End For
    Output
    primes up to 10000: 1229
    primes up to 20000: 2262
    primes up to 40000: 4203

    The tasks finish in any order, but each Wait() gives its own task’s result, so the output is the same every run.

    What work can use

    Work on another thread can use only values that can’t change under it and that are safe to share: a Const, a parameter, or a loop variable, holding a Sendable value, like a number, a String, or a Struct of them. A Local could change while the work runs, so capturing one is an error, and an ordinary Class’s object belongs to the thread that made it, so a Const that holds one can’t go either:

    Import std.core
    
    Class Box
        Field Value: Int
    End Class
    
    Local n = 10
    Local doubled = Spawn(Function() => n * 2)
    
    Const box = Box()
    Local read = Spawn(Function() => box.Value)
    Print(doubled.Wait(), read.Wait())
    $ fb run main.fb
       Compiling main.fb
    main.fb:8:23: error: 'n' is a Local, which another thread can't share; make it a Const
      |
    8 | Local doubled = Spawn(Function() => n * 2)
      |                       ^^^^^^^^^^^^^^^^^^^
    main.fb:11:14: error: this work runs on another thread, but it uses the Const 'box', which holds Box, which isn't Sendable
       |
    11 | Local read = Spawn(Function() => box.Value)
       |              ^^^^^
    main.fb:11:34: note: here
       |
    11 | Local read = Spawn(Function() => box.Value)
       |                                  ---
    error: couldn't build main.fb because of 2 errors

    These are compile errors, so a data race doesn’t build. Sendable and Shared Class covers what can be shared.

    A Thread of its own

    A pool thread is meant for work that computes and finishes. Work that runs for a long time, or waits most of the time, like a loop that blocks on a socket, gets a thread of its own with Thread.Start(work), so it doesn’t hold a pool thread. Join() waits for it.

    Import std.core
    
    Local worker = Thread.Start(Function() => Print("on a thread of its own"))
    worker.Join()
    Print(worker.IsDone())
    Output
    on a thread of its own
    True

    When top-level code ends, the program waits for every Task and Thread it started before it ends. A panic on any thread ends the whole program.

    What you learned

    • Spawn(work) runs work on the worker pool and gives a Task<T>; Wait() gives its result.
    • Work on another thread captures only Consts, parameters, and loop variables of Sendable types, which the compiler checks.
    • Thread.Start(work) gives long-running work a thread of its own, and Join() waits for it.
    • The program waits for its Tasks and Threads when its top-level code ends.

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