module Lapis::Docs::D_CONCURRENCY_AND_FIBERS::A_FIBERS_AND_COOPERATIVE_AWAIT

Overview

Fibers & Cooperative Signal Awaiting

Comprehensive guide to running Crystal fibers in Godot's single-threaded scene loop, cooperative yielding, and avoiding the top-level sleep deadlock trap.

Executive Summary & Key Topics

Topic Method / Anchor Description
The Three Golden Fiber Invariants .topic_00_fiber_rules Essential rules for writing asynchronous fiber code in LibGodot.
Yielding Execution in _process .topic_01_cooperative_yielding Pumping spawned fibers cooperatively from the engine main thread.
The Sleep Deadlock Hazard .topic_02_the_sleep_deadlock_hazard Why sleep hangs in fibers and how to await timers safely.
AwaitStatus, Bang (!), and Predicate (?) Await Paradigms .topic_03_await_status_and_predicates Handling timeouts cleanly using AwaitStatus, exception-raising await!, and nilable/boolean await?.

Related Guides & Source References

Defined in:

libgodot/docs/d_concurrency_and_fibers/a_fibers_and_cooperative_await.cr

Class Method Summary

Class Method Detail

def self.topic_00_fiber_rules : Nil #

The Three Golden Fiber Invariants: Essential rules for writing asynchronous fiber code in LibGodot.

Key Topics & Information

  • Rule 1: Godot owns the OS main loop; fibers must cooperatively yield via Fiber.yield
  • Rule 2: NEVER call top-level sleep in a fiber; Godot does not pump LibEvent
  • Rule 3: Use await(timer) or frame delta accumulators for time delays

def self.topic_01_cooperative_yielding : Nil #

Yielding Execution in _process: Pumping spawned fibers cooperatively from the engine main thread.

Because Godot controls the OS event loop, spawned fibers (spawn do ... end) will starve unless the main thread cooperatively yields:

node GameManager < Node do
  def _ready : Void
    # Spawn a background gameplay fiber
    spawn do
      Godot.print("Dialog sequence started!")
      await(get_tree.create_timer(2.0))
      Godot.print("2 seconds elapsed non-blockingly!")
    end
  end

  def _process(delta : Float64) : Void
    # Cooperatively yield time slice to pending fibers:
    Fiber.yield
  end
end

def self.topic_02_the_sleep_deadlock_hazard : Nil #

The Sleep Deadlock Hazard: Why sleep hangs in fibers and how to await timers safely.

The Hazard:

In standard Crystal console programs, sleep 2.seconds registers a timer with Crystal's LibEvent / IOCP event loop. In LibGodot, Godot drives the main loop — not LibEvent! Calling top-level sleep suspends the fiber permanently, hanging that fiber indefinitely.

The Safe Pattern:

Always use Godot's timer mechanism with await:

# SAFE: Suspends fiber until Godot's timer fires
await(get_tree.create_timer(1.5))

def self.topic_03_await_status_and_predicates : Nil #

AwaitStatus, Bang (!), and Predicate (?) Await Paradigms: Handling timeouts cleanly using AwaitStatus, exception-raising await!, and nilable/boolean await?.

LibGodot provides three complementary await idioms to distinguish between successful signal emissions and timeouts without ambiguous empty return values:

Paradigm On Success On Timeout Primary Use Case
await(...) AwaitStatus(T).completed(val) AwaitStatus(T).timed_out Inspection via status.completed? or status.timed_out? without raising exceptions
await!(...) Unwrapped T directly Raises Godot::TimeoutError Fail-fast operations, strict handshakes, and turn-based RPC sequences
await?(...) val (or true for 0-arg/timers) nil (or false for 0-arg/timers) Quick-time events (QTEs), guard checks, and non-raising optional reactions

1. AwaitStatus Inspection (await):

status = await(boss.phase_changed, timeout_sec: 5.0)
if status.completed?
  Godot.print("Boss entered phase: #{status.value!}")
else
  Godot.print("Phase change timed out!")
end

2. Fail-Fast Unwrapping (await!):

# Returns unwrapped tuple directly or raises Godot::TimeoutError
score, bonus = node.level_scored.await!(timeout_sec: 10.0)

3. Nilable Predicates (await?):

# Returns true if parried in time, false if timed out
if await?(player.parry_pressed, timeout_sec: 0.25)
  enemy.stagger!
else
  player.take_damage(50)
end