執行緒教學

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 宇宙中的所有 coroutines 都必須共用相同的 mutex。不要犯與特定 Lua 狀態關聯 mutex 的錯誤,然後在同一宇宙中不同的 coroutine 遭到鎖定時找不到它。以下是針對 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 時將會使用這三個預處理器定義。Lua 從 5.0.2 版開始,很遺憾地不提供呼叫來摧毀鎖定。(VersionNotice: 為 Lua 5.1 更新?)

不論是用 lua_open 還是 lua_newthread 呼叫來建立新的 Lua 狀態,都會呼叫函數 lua_userstateopen。只有 lua_userstateopen 第一次呼叫時,才會建立 mutex 很重要。

在 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 使用鎖定函數來防止其內部資料結構遭到破壞。應用程式負責防止公開資料結構(不論是全域或 upvalues)出現問題。可以使用 mutex 來使用像是上面顯示的函數來協調資源使用。但是,一定要使用與 Lua 所使用的不同的 mutex,來避免潛在的死結。

在設計多執行緒應用程式時,要注意每個執行緒在何處等待會有所幫助。務必要記住沒有防護的作業可能會遭到中斷。如果其他執行緒因為中斷發生的狀態而受到不良影響,就會需要某種類型的互斥。

全域 mutex 和多處理

上述使用全域 mutex 來在 Lua 中實作執行緒的方法在多處理器系統上效率不彰。當一個執行緒保持全域 mutex 時,其他執行緒會等待該 mutex。因此,一次只可能執行一個 Lua 執行緒,不論系統中有多少處理器。

在某些系統中,可能需要在解鎖互斥鎖後讓出執行緒,以防範相同執行緒再次鎖定,當中有其他執行緒正在等候時情況尤其如此。至少已知採用 Boost.Threads 的 Linux 系統會發生這種情形。不妨試著覆寫 luai_threadyield(預設會呼叫 lua_unlock,接著馬上呼叫 lua_lock),讓執行緒在鎖定與解鎖之間讓出。不過,虛擬機器中的 dojump 巨集會呼叫 luai_threadyield,代表每次呼叫 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);
}

另請參閱


近期異動 · 偏好設定
編輯 · 歷史
最後編輯時間為 2011 年 1 月 1 日上午 1:41 GMT (diff)