Threads Tutorial

lua-users home
wiki

抢占式线程和 Lua

Lua 所遵循的标准 ANSI C 没有管理多个执行线程的机制。线程以及用于控制它们的同步对象由底层操作系统提供。你需要使用它们来实现 Lua 中的线程。你不需要修改 Lua 分发版来做到这一点。

即使在最简单的 C 应用程序中,线程也很难正确处理。嵌入或扩展 Lua 的应用程序必须处理将线程与 Lua 库协调的额外复杂性。如果你的多任务需求可以通过 Lua 的单线程协程来满足,那么选择这条路线是明智的。欲了解更多详情,请阅读 CoroutinesTutorial。如果你确实选择为你的 Lua 项目实现多个抢占式线程,以下指南可能会有所帮助。

与 Lua 交互的每个 C 线程都需要自己的 Lua 状态。这些状态中的每一个都有自己的运行时堆栈。当启动一个新的 C 线程时,你可以通过两种方式之一创建它的 Lua 状态。一种方法是调用 lua_open。这会创建一个新状态,该状态独立于其他线程中的状态。在这种情况下,你需要像初始化新程序的状态一样初始化 Lua 状态(例如,加载库)。这种方法消除了对互斥锁(下文讨论)的需求,但会阻止线程共享全局数据。

另一种方法是调用 lua_newthread。这会创建一个子状态,它有自己的堆栈并且可以访问全局数据。这里讨论的是这种方法。

Lua 的锁定

使用线程时,你最大的担忧是防止它们相互破坏环境。线程在抢占后返回到一个被另一个线程置于意外状态的环境中,会带来细微或不那么细微、立即或有时令人抓狂的延迟的问题。然而,你通常也希望线程共享某些数据结构。这就是操作系统的互斥对象发挥作用的地方。这种类型的对象一次只能被一个线程锁定。

在你的帮助下,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 遗憾地不提供销毁锁的调用。(VersionNotice:为 Lua 5.1 更新?)

每当创建新的 Lua 状态时(无论是通过调用 lua_open 还是 lua_newthread),都会调用函数 lua_userstateopen。重要的是,互斥锁应该只在第一次调用 lua_userstateopen 时创建。

在 Lua 5.1 中,对于使用 lua_newthread 创建的线程调用 luai_userstatethread(L,L1);对于由 lua_newstate 创建的 Lua 状态(但不是由 lua_newthread 创建)调用 luai_userstateopen(L)。对于由 lua_close 关闭的线程,才调用 luai_userstateclose(L)

相关的 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 通过锁定函数来防止其内部数据结构被破坏。应用程序需要负责防止暴露的数据结构(无论是全局的还是 upvalues)出现问题。可以使用互斥锁来协调资源使用,如上所示的函数。但是,请务必使用与 Lua 使用的互斥锁不同的互斥锁,以避免潜在的死锁。

在设计多线程应用程序时,特别注意每个线程的等待位置很有帮助。始终要意识到无保护的操作可能会被中断。如果任何其他线程可能受到中断发生时的状态的不利影响,则需要某种形式的互斥。

全局互斥锁和多处理

上述使用全局互斥锁在 Lua 中实现线程的方法在多处理器系统上效率低下。当一个线程持有全局互斥锁时,其他线程都在等待它。因此,无论系统有多少个处理器,一次只有一个 Lua 线程可以运行。

在某些系统上,解锁互斥锁后可能需要让出线程,以防止同一线程在有其他线程等待它时再次锁定它。这至少发生在 Linux 上使用 Boost.Threads。重写 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);
}

另请参阅


RecentChanges · preferences
编辑 · 历史
最后编辑于 2010 年 12 月 31 日 下午 7:41 GMT (差异)