Lua does not make a single state safely available to arbitrary preemptive threads. Instead, it offers two useful building blocks:
- cooperative threads, usually exposed as coroutines, with separate stacks inside one Lua state;
- independent Lua states, which do not share ordinary Lua memory and can be owned by separate native threads with explicit communication.
Threads inside one state
Most C API functions receive a lua_State *. The pointer identifies both the global Lua state and the particular thread stack being operated on. luaL_newstate() creates the state and its main thread. Additional cooperative threads are created with:
1 | lua_State *thread = lua_newthread(L); |
The new thread is also pushed as a Lua value on L. It is garbage-collected like other Lua objects, so keep it reachable from the registry, a Lua variable, or another anchored object while native code holds its pointer. Popping the only reference and then calling an allocating API can leave a dangling pointer.
Values can move between thread stacks that belong to the same state:
1 | lua_getglobal(thread, "f"); |
Resume and yield
To use the thread as a coroutine, push its entry function and arguments and call the lua_resume signature for your Lua version. It can finish, fail, or return LUA_YIELD. Values passed to coroutine.yield() remain on the yielded stack for the resumer to inspect.
The C API changed across Lua releases. For example, Lua 5.4 uses an nresults output parameter:
1 | int nresults = 0; |
Native C functions can yield only through the continuation APIs supported by the Lua version, such as lua_yieldk, because the native C stack itself cannot be preserved like a Lua stack frame.
Independent states
Create isolated states with luaL_newstate() and close each with lua_close(). Independent states have separate globals, registries, heaps, and garbage collectors. They cannot exchange values with lua_xmove; communication must serialize or copy data through an application-defined channel.
One operating-system thread may own each isolated state, but do not access the same state concurrently unless the embedding application provides strict synchronization. Also avoid invoking Lua on a state from a foreign thread while it is already running.
Cooperative threads are suited to pausing workflows without shared-memory races. Independent states are suited to isolation or parallel native ownership at the cost of explicit communication and duplicate runtime data.