Skip to content
FutureBlitz

    Install

    Concurrency, 7 of 8

    Coroutines and Schedulers

    Write sequences that take time as Async Functions that Await Wait, and run them on a Scheduler you tick yourself.

    What you’ll build

    Two countdowns that run side by side, each waiting between its numbers, written as plain loops. Then a fade from 100% to 0% on a Scheduler of the program’s own, whose time the program moves on itself, so it can run slower, faster, or stop.

    A coroutine

    A coroutine is an Async Function that waits for time rather than for I/O. Await Wait(time) pauses it for that long without blocking the thread, so a sequence that takes seconds is written as the steps it is. A time is std.time’s Duration, like 200.Milliseconds or 2.Seconds.

    Import std.core
    Import std.time
    
    Async Function Countdown(name: String, from: Int, gap: Duration)
        For i = from To 1 Step -1
            Print("{name}: {i}")
            Await Wait(gap)
        End For
        Print("{name}: go")
    End Function
    
    Countdown("slow", 2, 500.Milliseconds)
    Countdown("fast", 3, 200.Milliseconds)
    Print("top-level code is done")
    Output
    slow: 2
    fast: 3
    top-level code is done
    fast: 2
    fast: 1
    slow: 1
    fast: go
    slow: go

    Each call runs its countdown until its first Await, then returns its Task, so both have printed their first number before the last line of top-level code. When top-level code ends, the main scheduler runs the coroutines that are still waiting until none is left: fast goes on every 200 milliseconds, and slow every 500, on the main thread, one at a time.

    The main scheduler

    The main scheduler resumes the coroutines on the main thread. It runs by itself, at these points only:

    • at an Await in top-level code, while its Task isn’t done;
    • at a Task’s Wait() on the main thread, while the Task isn’t done;
    • once top-level code ends, until no coroutine waits on it;
    • at Scheduler.Main.Step(), which The main thread uses in a loop of the program’s own.

    Nothing else runs it. An Await in an Async Function only pauses that coroutine: the code that called it, or the scheduler that resumed it, goes on. So a coroutine never runs in the middle of other main-thread code, and main-thread code needs no locks. A call that blocks, like std.time’s Sleep, doesn’t run it either:

    Import std.core
    Import std.time
    
    Async Function Note(text: String)
        Await Wait(10.Milliseconds)
        Print(text)
    End Function
    
    Note("the coroutine goes on")
    Sleep(50.Milliseconds)
    Print("after Sleep")
    Await Wait(1.Milliseconds)
    Print("after the Await")
    Output
    after Sleep
    the coroutine goes on
    after the Await

    The coroutine’s 10 milliseconds are up during the Sleep, but it waits until the top-level Await runs the main scheduler.

    A Scheduler of your own

    Scheduler() makes a scheduler whose time moves only when the program says. Run(task) moves a coroutine onto it, and Tick(seconds) moves its time on by that much and resumes whatever is due. This fade waits two seconds between steps, in its scheduler’s time:

    Import std.core
    Import std.time
    
    Async Function Fade(name: String)
        For level = 100 To 0 Step -25
            Print("{name} at {level}%")
            Await Wait(2.Seconds)
        End For
    End Function
    
    Local playback = Scheduler()
    playback.Run(Fade("title"))
    
    For step = 1 To 7
        Local seconds = step <= 4 ? 1.0 : 2.0
        Print("tick {step}: {seconds} s")
        playback.Tick(seconds)
    End For
    Print(playback.IsIdle())
    Output
    title at 100%
    tick 1: 1 s
    tick 2: 1 s
    title at 75%
    tick 3: 1 s
    tick 4: 1 s
    title at 50%
    tick 5: 2 s
    title at 25%
    tick 6: 2 s
    title at 0%
    tick 7: 2 s
    True

    The first four ticks are a second each, so the fade takes a step every other tick. The ticks after that are two seconds each, and the fade takes a step every tick, twice as fast. A tick resumes a coroutine once at most, however long it is. No real time passes: a Scheduler’s time is only what its ticks add, which is what pausing and speeding things up need. Not ticking it pauses everything on it, and IsIdle() says when nothing waits on it.

    Await Wait(time) counts the time of the scheduler the coroutine runs on, with its pauses and speed, and Await RealWait(time) counts the real clock’s, ignoring both.

    What the main scheduler can’t do

    The main scheduler’s time is the real clock’s, so ticking it is an error, which says what to call instead:

    Import std.core
    
    Scheduler.Main.Tick(1.0)
    $ fb run main.fb
       Compiling main.fb
    main.fb:3:1: error: Scheduler.Main's time is the real clock's, which a Tick can't move; call Scheduler.Main.Step() instead
      |
    3 | Scheduler.Main.Tick(1.0)
      | ^^^^^^^^^^^^^^^^^^^
    error: couldn't build main.fb because of 1 error

    And coroutines run only on the main thread, so work on another thread can’t start one:

    Import std.core
    Import std.time
    
    Async Function Later()
        Await Wait(1.Seconds)
    End Function
    
    Spawn(Function() => Later())
    $ fb run main.fb
       Compiling main.fb
    main.fb:8:1: error: this work runs on another thread, but it calls 'Later', which is Async, so its code runs on the main thread
      |
    8 | Spawn(Function() => Later())
      | ^^^^^
    main.fb:8:21: note: here
      |
    8 | Spawn(Function() => Later())
      |                     -----
    error: couldn't build main.fb because of 1 error

    What you learned

    • A coroutine is an Async Function that Awaits Wait(time) between its steps.
    • The main scheduler resumes coroutines on the main thread, at top-level Awaits and Wait()s of Tasks that aren’t done, at Steps, and at the end of top-level code.
    • Scheduler() makes a scheduler whose time moves only with Tick, for pausing and scaling time.
    • Run(task) moves a coroutine onto a Scheduler, and RealWait waits by the real clock.

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