Concurrency, 8 of 8
The main thread
Keep main-thread code on the main thread with @MainThread, and run coroutines from a loop of your own with Scheduler.Main.Step.
What you’ll build
A program with a loop of its own, like a terminal UI’s redraws or a server’s poll, that lets a coroutine go on between its passes. You’ll mark a function that only the main thread may call, and see the two ways a loop runs the main scheduler.
One thread for most code
A program starts on the main thread. Its top-level code runs there, and so does all Async code,
every coroutine included. Only the work given to Spawn, Thread.Start, and Parallel For runs
elsewhere. That’s why main-thread code needs no locks: nothing else runs it, and a coroutine goes
on only at an Await, a Wait(), or a Step.
Some things are the main thread’s alone: a plain Global, which isn’t safe to share, and APIs that
a library ties to one thread, like a UI toolkit’s windows. @MainThread marks a function or Method
that only the main thread may call.
Import std.core
Global title = "untitled"
@MainThread
Function SetTitle(text: String)
title = text
End Function
SetTitle("report")
Print(title)report
Work on another thread that calls it, directly or through what it calls, is a compile error:
Import std.core
Global title = "untitled"
@MainThread
Function SetTitle(text: String)
title = text
End Function
Local done = Spawn(Function() => SetTitle("report"))
done.Wait() Compiling main.fb
main.fb:10:14: error: this work runs on another thread, but it calls 'SetTitle', which is @MainThread
|
10 | Local done = Spawn(Function() => SetTitle("report"))
| ^^^^^
main.fb:10:34: note: here
|
10 | Local done = Spawn(Function() => SetTitle("report"))
| --------
error: couldn't build main.fb because of 1 errorA loop of your own
When top-level code ends, the main scheduler runs the waiting coroutines by itself. A program that
runs a loop of its own, and doesn’t end, lets them go on between its passes with
Scheduler.Main.Step(). Step resumes the coroutines that can go on, and returns at once; it never
waits.
Import std.core
Async Function Announce(steps: Int)
For i = 1 To steps
Print(" coroutine, step {i}")
Await NextTick()
End For
Print(" coroutine done")
End Function
Announce(3)
For pass = 1 To 4
Print("pass {pass}")
Scheduler.Main.Step()
End Forcoroutine, step 1 pass 1 coroutine, step 2 pass 2 coroutine, step 3 pass 3 coroutine done pass 4
Await NextTick() waits for the next time the scheduler runs, so the coroutine takes one step per
pass. A coroutine that awaits Wait(time) goes on at the first Step after its time has come.
Await in the loop
An Await in top-level code runs the main scheduler too, as a Wait() does. Await NextTick()
in the loop gives the coroutines one turn, as Step does:
Import std.core
Async Function Announce(steps: Int)
For i = 1 To steps
Print(" coroutine, step {i}")
Await NextTick()
End For
Print(" coroutine done")
End Function
Announce(3)
For pass = 1 To 4
Print("pass {pass}")
Await NextTick()
End Forcoroutine, step 1 pass 1 coroutine, step 2 pass 2 coroutine, step 3 pass 3 coroutine done pass 4
Use Scheduler.Main.Step() where the loop does its own waiting, as for input, and Await where it
waits on a Task anyway. A library that owns a loop, like a UI toolkit’s event loop, calls Step for
its users, so a program built on one never needs to.
What you learned
- Top-level code and all Async code run on the main thread; only Spawn’s, Thread.Start’s, and Parallel For’s work runs elsewhere.
@MainThreadmarks code that only the main thread may call, which the compiler checks.- A loop of the program’s own runs the coroutines with
Scheduler.Main.Step(), which never waits. Await NextTick()waits for the scheduler’s next turn, and in top-level code gives it one.
The rules are in the language reference.
Report an issue with this page or with FutureBlitz on GitHub.