Nanfeng

Notes on software development, code, and curious ideas

Lua Threads, Coroutines, and Independent States in the C API

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
2
3
4
lua_getglobal(thread, "f");
lua_pushinteger(thread, 5);
lua_call(thread, 1, 1);
lua_xmove(thread, L, 1);

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
2
3
4
5
6
7
8
9
int nresults = 0;
int status = lua_resume(thread, L, argumentCount, &nresults);

if (status == LUA_YIELD) {
/* Read yielded values from thread's stack. */
} else if (status != LUA_OK) {
const char *message = lua_tostring(thread, -1);
/* Report the error. */
}

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.

+