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())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 Forprimes 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())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())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 aTask<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, andJoin()waits for it.- The program waits for its Tasks and Threads when its top-level code ends.
The rules are in the language reference.
Report an issue with this page or with FutureBlitz on GitHub.