Skip to content

23 · Concurrency

Spawn, Timing, and Cancellation

spawn launches cooperative tasks; every/after schedule work over time; cancellation uses shared flags.

On this page

spawn registers a task with the cooperative scheduler — it does not run immediately. The scheduler alternates between ready tasks when the caller invokes tick(), every, after, or at the end of a scope.

The tick() call is equivalent to coroutine.yield(0): it returns control to the scheduler so another ready task can run. In long loops, inserting tick() is mandatory to avoid monopolizing the CPU.

Two spawn tasks alternating with tick(); producer/consumer via a shared queue.

04-spawn-tick.zolo
Playground
// Feature: spawn + tick — parallel cooperative tasks

// Syntax: `spawn { ... }` or `spawn fn_call(...)`; `tick()` yields control.

// When to use: single-threaded cooperative parallelism for

// simulations, games, animations, lightweight agents.

//

// The internal scheduler drains each task. `tick()` is equivalent to

// `coroutine.yield(0)` — hands control back to the scheduler so it

// picks another ready task. Without `tick()`, a task hogs the CPU.


fn counter(id: int, max: int) {
  var n = 0
  while n < max {
    print("counter {id}: {n}")
    n += 1
    tick()
  }
}

print("=== spawn + named function ===")
spawn counter(1, 3)
spawn counter(2, 3)

// `spawn { ... }` — anonymous block, direct.

print("=== spawn block ===")
spawn {
  var i = 0
  while i < 3 {
    print("block A: {i}")
    i += 1
    tick()
  }
}
spawn {
  var i = 0
  while i < 3 {
    print("block B: {i}")
    i += 1
    tick()
  }
}

// Simple producer/consumer with a shared queue.

let queue: [int] = []
var produced = 0
var consumed = 0

spawn {
  var i = 0
  while i < 5 {
    queue.push(i)
    produced += 1
    tick()
    i += 1
  }
}

spawn {
  while consumed < 5 {
    if queue.len() > 0 {
      let v = queue.shift()
      consumed += 1
      print("consumed: {v}")
    }
    tick()
  }
}
// The scheduler drains automatically once the top-level finishes.

To schedule work over time, use every <duration> { ... } (periodic loop) and after <duration> { ... } (single deferred execution). Both are sugar for spawn + loop + tick(<dur>) and can coexist in parallel.

Game loop with every 50ms, one-shot firing with after, and two parallel every blocks at different rates.

05-every-after.zolo
Playground
// Feature: every / after — cooperative temporal loops

// Syntax: `every <duration> { body }`, `after <duration> { body }`

// When to use: games (game loop), animations, polling, lightweight

// scheduling without needing OS threads.

//

// `every` is sugar for `spawn + loop + tick(<dur>)`. `after` schedules

// the body to run a single time after the duration.


// Simple game loop: tick every 50ms until 5 frames.

var frame = 0
every 50ms {
  frame += 1
  print("frame {frame}")
  if frame >= 5 {
    break
  }
}

// `after` — single deferred execution.

print("before after")
after 10ms {
  print("this runs 10ms later")
}

// `every` without a duration = on every tick (most frequent).

var i = 0
every {
  i += 1
  print("fast tick {i}")
  if i >= 3 {
    break
  }
}

// Combining: two `every` loops in parallel.

var a = 0
var b = 0
every 30ms {
  a += 1
  print("A {a}")
  if a >= 3 { break }
}
every 50ms {
  b += 1
  print("B {b}")
  if b >= 3 { break }
}

Since the model is cooperative, there is no task.kill(). Cancellation is done via a shared flag that the task checks at each tick(). This pattern ensures that state is never corrupted mid-way.

Flag-based cancellation and a deadline pattern with after signaling a sentinel.

08-cancellation.zolo
Playground
// Feature: cooperative cancellation of tasks

// Syntax: shared flag/sentinel + check on every `tick()`.

// When to use: long tasks that need to stop when the user

// cancels, the window closes, or a deadline is reached.

//

// Since spawn is cooperative, cancellation is too: the task must

// ASK whether it was canceled. There is no forced kill — that is a

// feature, not a bug (avoids corrupted state).


let cancel = #{requested: false}

fn long_task(id: int) {
  var step = 0
  while step < 1000 {
    if cancel.requested {
      print("task {id} canceled at step {step}")
      return
    }
    if step % 3 == 0 {
      print("task {id}: step {step}")
    }
    step += 1
    tick()
  }
  print("task {id} finished normally")
}

spawn long_task(1)
spawn long_task(2)

// A supervisor yields a fixed number of scheduler cycles and then signals
// cancellation. A cycle budget keeps this example deterministic across the VM
// and ahead-of-time backends; a real application may set the same flag from a
// button, a deadline timer, a signal handler, or another task.
spawn {
  var cycles = 0
  while cycles < 3 {
    cycles += 1
    tick()
  }
  cancel.requested = true
  print(">> cancellation requested")
}

Challenge

Add a second paused flag to the cancellation example. The task should stop printing while paused is true, but resume when reactivated.

Global index

Find your way through Zolo

Try an idea

Start here

9 results

9 results

enespt-br