Skip to content

WebGL / single-threaded WASM: coroutine.yield deadlocks when ExecuteAsync is driven via GetAwaiter().GetResult() #327

Description

@NeoXider

Summary

On single-threaded runtimes (Unity WebGL / IL2CPP → WASM), a script that calls coroutine.yield deadlocks the only thread when the ExecuteAsync ValueTask is consumed synchronously via GetAwaiter().GetResult(). Pure-compute scripts run fine; only paths that actually suspend the async state machine (coroutines) hang.

Environment

  • Unity 6000.x, IL2CPP, WebGL player (single-threaded WASM, no thread pool)
  • Lua-CSharp (latest NuGet LuaCSharp, Lua.dll, netstandard2.1)
  • Desktop Mono/.NET (Windows) and Android IL2CPP work fine — threads available.

Repro (conceptual)

var state = LuaState.Create();
state.OpenBasicLibrary(); state.OpenCoroutineLibrary(); /* string/table/math ... */
LuaValue[] Run(LuaClosure c) => state.ExecuteAsync(c).GetAwaiter().GetResult();

// OK on WebGL:
Run(state.Load("return (2+3)*4"));

// HANGS on WebGL (never returns, freezes the player):
Run(state.Load([[
  local co = coroutine.create(function(a) local b = coroutine.yield(a+1); return b*2 end)
  local _, x = coroutine.resume(co, 10)
  local _, y = coroutine.resume(co, 5)
  return x + y]]));

Observed

  • arithmetic / strings / tables / closures / recursion / metatables: all correct on WebGL.
  • the coroutine case: ExecuteAsync(...).GetAwaiter().GetResult() never returns; the WASM main thread is blocked permanently.

Analysis

coroutine.yield genuinely suspends the async state machine, so ExecuteAsync returns an incomplete ValueTask whose continuation must be pumped. On desktop the continuation runs on another thread while the caller blocks, so .GetResult() eventually returns. On single-threaded WASM there is no other thread, and the only thread is parked in the blocking wait, so the continuation can never run — a classic sync-over-async deadlock.

Ask

Would you consider: (1) documenting the single-threaded / WebGL constraint and the recommended way to drive ExecuteAsync without blocking (pumping the ValueTask across frames on the main thread), ideally with a small sample; and/or (2) clarifying whether coroutines are expected to work under a synchronous drive on single-threaded hosts?

Thanks for the library — on desktop/Android it is noticeably faster and lighter than MoonSharp in our benchmarks; we are evaluating it as a Unity modding runtime and this is the one remaining blocker for WebGL parity.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions