Pre-emptive threads and Lua
ANSI C, the standard to which Lua complies, has no mechanism for managing multiple threads of execution. Threads and the synchronization objects used to control them are provided by the underlying operating system. You'll need to use these in order to implement threading in Lua. You will not need to modify the Lua distribution to do this.
Threads are tricky to get right even in the simplest of circumstances such as a pure C application. An application which embeds or extends Lua must cope with the additional complexity of coordinating threads with the Lua library. If your multitasking needs can be met with Lua's single-threaded coroutines, you would be well-advised to choose that route. Read CoroutinesTutorial for more details. If you do choose to implement multiple pre-emptive threads to your Lua project, the following guidelines may help.
Each thread in C which interacts with Lua will need its own Lua state. Each of these states has its own runtime stack. When a new C thread is started, you can create its Lua state in one of two ways. One way is to call lua_open
. This creates a new state which is independent of the states in other threads. In this case, you'll need to initialize the Lua state (for example, loading libraries) as if it was the state of a new program. This approach eliminates the need for mutex locks (discussed below), but will keep the threads from sharing global data.
The other approach is to call lua_newthread
. This creates a child state which has its own stack and which has access to global data. This approach is discussed here.
Locking by Lua
Your biggest concern when working with threads is to prevent them from corrupting one another's environments. Subtle or not-so-subtle problems, immediate or sometimes maddeningly delayed, await a thread which returns after pre-emption to an environment which has been left in an unexpected state by another thread. However, you generally want threads to share certain data structures, too. This is where mutex objects from the operating system come into play. An object of this type can be locked by no more than one thread at a time.
With some help from you, Lua will prevent its internal data structures from being corrupted. When Lua enters into an operation which must not be pre-empted, it calls lua_lock
. When the critical operation is complete, it calls lua_unlock
. In the default distribution these two functions do nothing. When using threads in Lua, they should be replaced with OS-dependent implementations. In a POSIX environment you'll use an object of type pthread_mutex_t
. In Windows, you'll use either a handle which is returned from CreateMutex
, or, more optimally, an opaque data structure of type CRITICAL_SECTION
.
All coroutines within a particular lua universe must share the same mutex. Avoid the mistake of associating the mutex with a specific Lua state and then failing to find it again when a different coroutine within the same universe is locked. A simple example for Win32 follows. A custom header file luauser.h
might contain:
-
#define lua_lock(L) LuaLock(L) #define lua_unlock(L) LuaUnlock(L) #define lua_userstateopen(L) LuaLockInitial(L) #define lua_userstatethread(L,L1) LuaLockInitial(L1) // Lua 5.1 void LuaLockInitial(lua_State * L); void LuaLockFinal(lua_State * L); void LuaLock(lua_State * L); void LuaUnlock(lua_State * L);
The three preprocessor definitions will be used when Lua is compiled. As of version 5.0.2, Lua regrettably does not provide a call to destroy a lock. (VersionNotice: update for Lua 5.1 ?)
The function lua_userstateopen
will be called whenever a new Lua state is created, whether with a call to lua_open
or lua_newthread
. It is important that the mutex be created only the first time lua_userstateopen
is called.
In Lua 5.1 luai_userstatethread(L,L1)
is called for threads created with lua_newthread
. and luai_userstateopen(L)
is called for lua states created by lua_newstate
(but not by lua_newthread
). luai_userstateclose(L)
is called for threads closed by lua_close
only.
The associated C file, luauser.c
, contains:
-
#include <windows.h> #include "lua.h" #include "luauser.h" static struct { CRITICAL_SECTION LockSct; BOOL Init; } Gl; void LuaLockInitial(lua_State * L) { if (! Gl.Init) { /* Create a mutex */ InitializeCriticalSection(&Gl.LockSct); Gl.Init = TRUE; } } void LuaLockFinal(lua_State * L) /* Not called by Lua. */ { /* Destroy a mutex. */ if (Gl.Init) { DeleteCriticalSection(&Gl.LockSct); Gl.Init = FALSE; } } void LuaLock(lua_State * L) { /* Wait for control of mutex */ EnterCriticalSection(&Gl.LockSct); } void LuaUnlock(lua_State * L) { /* Release control of mutex */ LeaveCriticalSection(&Gl.LockSct); }
These two files need not reside in the Lua distribution tree, but they need to be accessible during the build. Additionally, you'll need to define LUA_USER_H
so that your include file is used by Lua. The quotes need to be part of the definition, so that an expression something like
-
/DLUA_USER_H="""luauser.h"""
will need to be conveyed to the compiler. Also, you may need to extend the include path so that the compiler can find your file.
Locking by the application
Lua prevents its internal data structures from becoming corrupted by means of the locking functions. It is up to the application to prevent problems with exposed data structures, whether they are global or upvalues. A mutex can be used to coordinate resource use using functions like the ones show above. However, be sure to use a different mutex than the one used by Lua in order to avoid potential deadlocks.
When designing multi-threaded applications, it is helpful to pay particular attention to where each thread waits. Always be aware that unguarded operations can be interrupted. If any other thread could be adversely affected by the state in which an interruption occurs, some form of mutual exclusion is called for.
Global mutex and multiprocessing
The method above for implementing threading in Lua using a global mutex is inefficient on multiprocessor systems. As one thread holds the global mutex, other threads are waiting for it. Thus only one Lua thread may be running at a time, regardless of the number of processors in the system.
On some systems it might be necessary to yield the thread after unlocking the mutex, in order to prevent the same thread from locking it again, in case there are others waiting on it. This happens at least on Linux using Boost.Threads. Overriding luai_threadyield
(which by default calls lua_unlock
, followed immediately by lua_lock
) to yield the thread inbetween the lock and the unlock is probably a good idea. However, luai_threadyield is called by the dojump macro in the virtual machine, hence a yield at every call of luai_threadyield might considerably degrade performance. The following alternative might be useful:
void luai_threadyield(struct lua_State *L) { static int count=0; bool y=false; if (count--<=0) { y=true; count=30; }; // Try different values instead of 30. lua_unlock(L); if (y) thread::yield(); lua_lock(L); }