Skip to content
FutureBlitz

    Install

    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)
    Output
    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()
    $ fb run main.fb
       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 error

    A 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 For
    Output
      coroutine, 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 For
    Output
      coroutine, 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.
    • @MainThread marks 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.

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