Lua的線程和狀態

【那不是真的多線程】

Lua不支持真正的多線程,這句話我在《Lua中的協同程序》這篇文章中就已經說了。根據個人編程經驗,在開發過程當中,若是能夠避免使用線程,那就堅定不用線程,若是實在沒有更好的辦法,那就只能退而用之。爲何?首先,多個線程之間的通訊比較麻煩,同時,線程之間共享內存,對於共享資源的訪問,使用都是一個很差控制的問題;其次,線程之間來回切換,也會致使一些不可預估的問題,對性能也是一種損耗。Lua不支持真正的多線程,而是一種協做式的多線程,彼此之間協做完成,並非搶佔完成任務,因爲這種協做式的線程,所以能夠避免由不可預知的線程切換所帶來的問題;另外一方面,Lua的多個狀態之間不共享內存,這樣便爲Lua中的併發操做提供了良好的基礎。html

【多個線程】

從C API的角度來看,將線程想象成一個棧可能更形象些。從實現的觀點來看,一個線程的確就是一個棧。每一個棧都保留着一個線程中全部未完成的函數調用信息,這些信息包括調用的函數、每一個調用的參數和局部變量。也就是說,一個棧擁有一個線程得以繼續運行的全部信息。所以,多個線程就意味着多個獨立的棧。編程

當調用Lua C API中的大多數函數時,這些函數都做用於某個特定的棧。當咱們調用lua_pushnumber時,就會將數字壓入一個棧中,那麼Lua是如何知道該使用哪一個棧的呢?答案就在類型lua_State中。這些C API的第一個參數不只表示了一個Lua狀態,還表示了一個記錄在該狀態中的線程。windows

只要建立一個Lua狀態,Lua就會自動在這個狀態中建立一個新線程,這個線程稱爲「主線程」。主線程永遠不會被回收。當調用lua_close關閉狀態時,它會隨着狀態一塊兒釋放。調用lua_newthread即可以在一個狀態中建立其餘的線程。多線程

lua_State *lua_newthread(lua_State *L);

這個函數返回一個lua_State指針,表示新建的線程。它會將新線程做爲一個類型爲「thread」的值壓入棧中。若是咱們執行了:併發

L1 = lua_newthread(L);

如今,咱們擁有了兩個線程L和L1,它們內部都引用了相同的Lua狀態。每一個線程都有其本身的棧。新線程L1以一個空棧開始運行,老線程L的棧頂就是這個新線程。函數

除了主線程之外,其它線程和其它Lua對象同樣都是垃圾回收的對象。當新建一個線程時,線程會壓入棧,這樣能確保新線程不會成爲垃圾,而有的時候,你在處理棧中數據時,不經意間就把線程彈出棧了,而當你再次使用該線程時,可能致使找不到對應的線程而程序崩潰。爲了不這種狀況的發生,能夠保持一個對線程的引用,好比在註冊表中保存一個對線程的引用。性能

當擁有了一個線程之後,咱們就能夠像主線程那樣來使用它,之前博文中提到的對棧的操做,對這個新的線程都適用。然而,使用多線程的目的不是爲了實現這些簡單的功能,而是爲了實現協同程序。ui

爲了掛起某些協同程序的執行,並在稍後恢復執行,咱們可使用lua_resume函數來實現。lua

int lua_resume(lua_State *L, int narg);

lua_resume能夠啓動一個協同程序,它的用法就像lua_call同樣。將待調用的函數壓入棧中,並壓入其參數,最後在調用lua_resume時傳入參數的數量narg。這個行爲與lua_pcall相似,但有3點不一樣。spa

  1. lua_resume沒有參數用於指出指望的結果數量,它老是返回被調用函數的全部結果;
  2. 它沒有用於指定錯誤處理函數的參數,發生錯誤時不會展開棧,這就能夠在發生錯誤後檢查棧中的狀況;
  3. 若是正在運行的函數交出(yield)了控制權,lua_resume就會返回一個特殊的代碼LUA_YIELD,並將線程置於一個能夠被再次恢復執行的狀態。

當lua_resume返回LUA_YIELD時,線程的棧中只能看到交出控制權時所傳遞的那些值。調用lua_gettop則會返回這些值的數量。若要將這些值移到另外一個線程,可使用lua_xmove。

爲了恢復一個掛起線程的執行,能夠再次調用lua_resume。在這種調用中,Lua假設棧中全部的值都是由yield調用返回的,固然了,你也能夠任意修改棧中的值。做爲一個特例,若是在一個lua_resume返回後與再次調用lua_resume之間沒有改變過線程棧中的內容,那麼yield剛好返回它交出的值。若是能很好的理解這個特例是什麼意思,那就說明你已經很是理解Lua中的協同程序了,若是你仍是不知道我說的這個特例是什麼意思,請再去讀一遍《Lua中的協同程序》,若是你還不懂,那你就在下放留言吧(提醒:這個特例主要利用的是resume-yield之間的傳參規則)。

如今,我就經過一個簡單的程序來作個試驗,以便更好的理解Lua的線程。使用C代碼來調用Lua腳本,Lua函數做爲一個協同程序來啓動,這個Lua函數能夠調用其它Lua函數,任意的一個Lua函數均可以交出控制權,從而使lua_resume調用返回。對於使用C調用Lua不熟悉的夥計,請再去仔細的讀讀《Lua與C》和《C「控制」Lua》這兩篇文章吧。先貼上重要的代碼吧。下面是Lua代碼:

function Func1(param1)
    Func2(param1 + 10)
    print("Func1 ended.")
    return 30
end

function Func2(value)
    coroutine.yield(10, value)
    print("Func2 ended.")
end

 

下面是C++代碼:

lua_State *L1 = lua_newthread(L);
if (!L1)
{
    return 0;
}

lua_getglobal(L1, "Func1");
lua_pushinteger(L1, 10);

// 運行這個協同程序
// 這裏返回LUA_YIELD
bRet = lua_resume(L1, 1);
cout << "bRet:" << bRet << endl;

// 打印L1棧中元素的個數
cout << "Element Num:" << lua_gettop(L1) << endl;

// 打印yield返回的兩個值
cout << "Value 1:" << lua_tointeger(L1, -2) << endl;
cout << "Value 2:" << lua_tointeger(L1, -1) << endl;

// 再次啓動協同程序
// 這裏返回0
bRet = lua_resume(L1, 0);
cout << "bRet:" << bRet << endl;
cout << "Element Num:" << lua_gettop(L1) << endl;
cout << "Value 1:" << lua_tointeger(L1, -1) << endl;

上面的程序,你能夠先運行一下;你能想到運行結果麼?單擊這裏下載完整工程LuaThreadDemo.zip

上面的例子是C語言調用Lua代碼,Lua能夠本身掛起本身;若是Lua去調用C代碼呢?C函數不能本身掛起它本身,一個C函數只有在返回時,纔會交出控制權。所以C函數其實是不會中止自身執行的,不過它的調用者能夠是一個Lua函數,那麼這個C函數調用lua_yield,就能夠掛起Lua調用者:

int lua_yield(lua_State *L, int nresults);

你沒有聽錯,C代碼調用lua_yield不能掛起本身,可是它卻能夠將它的Lua調用者掛起。其中nresults是準備返回給相應resume的棧頂值的個數,當協同程序再次恢復執行時,Lua調用者會收到傳遞給resume的值。lua_yield在使用時,只能做爲一個返回的表達式,而不能獨自使用。好比:

return lua_yield(L, 0);

對於多線程編程,自己就是麻煩的問題,而這裏枯燥的文字總結,也會沒有效果,下面來一個簡短的例子。先貼Lua代碼,這段代碼須要結合C代碼一塊兒看,不然就是雲裏霧裏的。

require "lua_yieldDemo"

local function1 = function ()
    local value
    repeat
      value = Module.Func1()
    until value
    return value
end

local thread1 = coroutine.create(function1)

-- 如今運行到了Module.Func1()
-- 100這個值將會被賦值給value
coroutine.resume(thread1)
--print(coroutine.status(thread1))

-- 設置C函數環境
Module.Func2(10)
print(coroutine.resume(thread1))

C代碼以下:

// 判斷環境表中JellyThink是否被設置了
static int IsSet(lua_State *L)
{
    lua_getfield(L, LUA_ENVIRONINDEX, "JellyThink");
    if (lua_isnil(L, -1))
    {
        printf("Not set\n");
        return 0;
    }
    return 1;
}

static int Func1(lua_State *L)
{
    // 沒有被設置就掛起
    if (!IsSet(L))
    {
        printf("Begin yield\n");
        return lua_yield(L, 0);
    }
    
    // 被設置了,就取值,返回被設置的值
    printf("Resumed again\n");
    lua_getfield(L, LUA_ENVIRONINDEX, "JellyThink");
    return 1;
}

// 設置JellThink的值
static int Func2(lua_State *L)
{
    luaL_checkinteger(L, 1);

    // 設置到環境表中
    lua_pushvalue(L, 1);
    lua_setfield(L, LUA_ENVIRONINDEX, "JellyThink");
    return 0;
}

當我在Lua中調用coroutine.resume時,我都只傳遞了一個參數,其它參數都沒有;這裏須要注意,若是我傳值了,就至關於給value賦值了。當我恢復thread1運行時,它是從Module.Func1()返回處繼續執行,也就是對value賦值,而這裏賦予value的值其實是傳給resume的值。上面的代碼中,我沒有傳值,若是傳了,就沒法驗證我設置的10了。單擊這裏下載完整工程lua_yieldDemo.zip。Any question? No? OK, Next.

【Lua狀態】

每次調用luaL_newstate(或者lua_newstate)都會建立一個新的Lua狀態。不一樣的Lua狀態是各自徹底獨立的,它們之間不共享任何數據。這個概念是否是很熟悉,是否是特別像Windows中的進程的概念。也就是說,在一個Lua狀態中發生的錯誤也不會影響其它的的Lua狀態,windows的進程也是這樣的。而且,Lua狀態之間不能直接溝通,必須寫一些輔助代碼來完成這點。

因爲全部交換的數據必須經由C代碼中轉,因此只能在Lua狀態間交換那些能夠在C語言中表示的類型,例如字符串和數字。因爲Lua狀態我目前沒有使用過,也就沒有足夠的信心和資格去總結這個東西,仍是怕會誤導你們,若是之後在實際項目中使用了Lua狀態,我還會回過頭來總結Lua狀態的。相信我,我還會回來的。

相關文章
相關標籤/搜索