-- Proof-of-concept: how Lua coroutines interact with the tokio runtime, and -- how the async os.sleep behaves in each setting. -- -- Run with: cargo run -- lua/coroutines-demo.lua local function banner(s) print("\n=== " .. s .. " ===") end ---------------------------------------------------------------------- banner("1. Pure Lua coroutines (no async involved)") ---------------------------------------------------------------------- -- Plain cooperative coroutines work exactly as in stock Lua: this is pure VM -- machinery and never touches tokio. local function counter(n) for i = 1, n do coroutine.yield(i * i) end return "done" end local co = coroutine.wrap(counter) print("squares:", co(3), co(3), co(3)) -- 1 4 9 local raw = coroutine.create(counter) print("status fresh:", coroutine.status(raw)) -- suspended coroutine.resume(raw, 1) coroutine.resume(raw, 1) -- runs to `return` print("status after return:", coroutine.status(raw)) -- dead ---------------------------------------------------------------------- banner("2. Async os.sleep at the top level") ---------------------------------------------------------------------- -- The main chunk is itself run by mlua's async executor (exec_async), so an -- async call here is driven correctly and really suspends on the tokio timer. local t0 = os.microtime() os.sleep(0.10) print(string.format("slept ~0.10s, measured %.3fs", os.microtime() - t0)) ---------------------------------------------------------------------- banner("3. GOTCHA: async os.sleep inside a *manually resumed* coroutine") ---------------------------------------------------------------------- -- mlua implements async functions by yielding a private sentinel to whatever is -- driving the coroutine. mlua's own executor understands it; a plain -- coroutine.resume / coroutine.wrap does NOT. So driving an async call yourself -- does not actually wait -- the sleep is not performed by your resume loop. local sleeper = coroutine.wrap(function() os.sleep(0.50) return "awoke" end) local t1 = os.microtime() local first = sleeper() -- returns immediately with the sentinel print(string.format("first resume returned %s after only %.3fs (did NOT wait 0.5s)", tostring(first), os.microtime() - t1)) print("--> lesson: don't hand-drive coroutines that call async stdlib functions.") ---------------------------------------------------------------------- banner("4. Real concurrency: task.join drives coroutines on tokio") ---------------------------------------------------------------------- -- task.join runs each function as its own coroutine and lets the tokio reactor -- interleave them. Three tasks that each sleep concurrently finish in ~the -- longest single sleep, not the sum -- proof the sleeps overlap on one thread. local function worker(name, secs) return function() print(string.format(" [%s] start", name)) os.sleep(secs) print(string.format(" [%s] woke after %.2fs", name, secs)) return name .. ":" .. secs end end local t2 = os.microtime() local a, b, c = task.join( worker("slow", 0.30), worker("med", 0.20), worker("fast", 0.10) ) local elapsed = os.microtime() - t2 print(string.format("results: %s, %s, %s", a, b, c)) print(string.format("wall time: %.3fs", elapsed)) print(string.format("sequential would have been ~0.60s; concurrent ~0.30s => %s", elapsed < 0.45 and "CONCURRENT (overlapped on tokio)" or "serialized?!")) ---------------------------------------------------------------------- banner("5. Nested + return values") ---------------------------------------------------------------------- -- task.join itself yields, so it composes: a joined task can join again. local outer = task.join( function() local x, y = task.join( function() os.sleep(0.05); return 21 end, function() os.sleep(0.05); return 21 end ) return x + y end, function() os.sleep(0.10); return "sibling" end ) print("nested join result:", outer) -- 42 print("\nAll demos finished.")