线程教程 |
|
ANSI C,Lua 遵循的标准,没有管理多个执行线程的机制。线程和用于控制它们的同步对象由底层操作系统提供。您需要使用它们才能在 Lua 中实现线程。您无需为此修改 Lua 发行版。
即使在最简单的环境(例如纯 C 应用程序)中,线程也很难正确使用。嵌入或扩展 Lua 的应用程序必须应对与 Lua 库协调线程的额外复杂性。如果您的多任务需求可以通过 Lua 的单线程协程满足,建议您选择该路线。阅读 协程教程 以了解更多详细信息。如果您选择在 Lua 项目中实现多个抢占式线程,以下指南可能会有所帮助。
与 Lua 交互的每个 C 线程都需要它自己的 Lua 状态。每个状态都有自己的运行时堆栈。当启动一个新的 C 线程时,您可以通过两种方式之一创建它的 Lua 状态。一种方法是调用 lua_open
。这将创建一个新的状态,该状态独立于其他线程中的状态。在这种情况下,您需要初始化 Lua 状态(例如,加载库),就好像它是新程序的状态一样。这种方法消除了对互斥锁(下面讨论)的需求,但会阻止线程共享全局数据。
另一种方法是调用 lua_newthread
。这将创建一个子状态,该状态有自己的堆栈,并且可以访问全局数据。这种方法将在本文中讨论。
在使用线程时,您最大的担忧是防止线程破坏彼此的环境。微妙或不那么微妙的问题,立即或有时令人抓狂的延迟,等待一个线程在抢占后返回到一个由另一个线程留下的意外状态的环境。但是,您通常希望线程共享某些数据结构。这就是操作系统中的互斥对象发挥作用的地方。这种类型的对象一次只能被一个线程锁定。
在您的帮助下,Lua 将防止其内部数据结构被破坏。当 Lua 进入必须不被抢占的操作时,它会调用 lua_lock
。当关键操作完成后,它会调用 lua_unlock
。在默认发行版中,这两个函数什么也不做。在 Lua 中使用线程时,应将它们替换为依赖于操作系统的实现。在 POSIX 环境中,您将使用类型为 pthread_mutex_t
的对象。在 Windows 中,您将使用从 CreateMutex
返回的句柄,或者更优化的方式,使用类型为 CRITICAL_SECTION
的不透明数据结构。
在一个特定的 Lua 宇宙中,所有协程必须共享同一个互斥锁。避免将互斥锁与特定的 Lua 状态关联,然后在同一个宇宙中的另一个协程被锁定时无法再次找到它。以下是一个简单的 Win32 示例。一个自定义头文件 `luauser.h` 可能包含
#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);
这三个预处理器定义将在编译 Lua 时使用。从 5.0.2 版本开始,Lua 可惜的是没有提供销毁锁的调用。(版本说明:更新到 Lua 5.1?)
函数 `lua_userstateopen` 将在每次创建新的 Lua 状态时调用,无论是通过调用 `lua_open` 还是 `lua_newthread`。重要的是,互斥锁只在第一次调用 `lua_userstateopen` 时创建。
在 Lua 5.1 中,`luai_userstatethread(L,L1)` 用于通过 `lua_newthread` 创建的线程,而 `luai_userstateopen(L)` 用于通过 `lua_newstate` 创建的 Lua 状态(但不是通过 `lua_newthread` 创建的)。`luai_userstateclose(L)` 仅用于通过 `lua_close` 关闭的线程。
关联的 C 文件 `luauser.c` 包含
#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); }
这两个文件不需要位于 Lua 分发树中,但它们需要在构建过程中可访问。此外,您需要定义 `LUA_USER_H`,以便您的包含文件被 Lua 使用。引号需要是定义的一部分,因此像这样的表达式
/DLUA_USER_H="""luauser.h"""
需要传递给编译器。此外,您可能需要扩展包含路径,以便编译器可以找到您的文件。
Lua 通过锁定函数阻止其内部数据结构被破坏。防止暴露数据结构(无论是全局的还是上值的)出现问题,是应用程序的责任。可以使用互斥锁来协调资源使用,使用上面显示的函数。但是,请确保使用与 Lua 使用的互斥锁不同的互斥锁,以避免潜在的死锁。
在设计多线程应用程序时,注意每个线程在哪里等待非常有用。始终注意,未保护的操作可能会被中断。如果任何其他线程可能受到中断发生时的状态的负面影响,则需要某种形式的互斥。
上面使用全局互斥锁在 Lua 中实现线程的方法在多处理器系统上效率低下。当一个线程持有全局互斥锁时,其他线程都在等待它。因此,无论系统中有多少处理器,一次只能运行一个 Lua 线程。
在某些系统上,可能需要在解锁互斥锁后让出线程,以防止同一个线程再次锁定它,以防有其他线程在等待它。这至少发生在使用 Boost.Threads 的 Linux 上。覆盖 `luai_threadyield`(默认情况下调用 `lua_unlock`,然后立即调用 `lua_lock`)以在锁定和解锁之间让出线程可能是一个好主意。但是,`luai_threadyield` 由虚拟机中的 `dojump` 宏调用,因此在每次调用 `luai_threadyield` 时让出可能会大大降低性能。以下替代方法可能有用
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); }