Skip to content

Spawn, Timing, and Cancellation

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)

// After a few iterations, signal cancellation.

var cycles = 0
every 10ms {
  cycles += 1
  if cycles >= 3 {
    cancel.requested = true
    print(">> cancellation requested")
    break
  }
}

// Deadline pattern: cancel after X ms.

let deadline = #{expired: false}
spawn {
  var n = 0
  while n < 1000 {
    if deadline.expired {
      print("deadline reached after {n} steps")
      return
    }
    n += 1
    tick()
  }
}

after 50ms {
  deadline.expired = true
}

Challenge

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

enespt-br