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
當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.
每次調用luaL_newstate(或者lua_newstate)都會建立一個新的Lua狀態。不一樣的Lua狀態是各自徹底獨立的,它們之間不共享任何數據。這個概念是否是很熟悉,是否是特別像Windows中的進程的概念。也就是說,在一個Lua狀態中發生的錯誤也不會影響其它的的Lua狀態,windows的進程也是這樣的。而且,Lua狀態之間不能直接溝通,必須寫一些輔助代碼來完成這點。
因爲全部交換的數據必須經由C代碼中轉,因此只能在Lua狀態間交換那些能夠在C語言中表示的類型,例如字符串和數字。因爲Lua狀態我目前沒有使用過,也就沒有足夠的信心和資格去總結這個東西,仍是怕會誤導你們,若是之後在實際項目中使用了Lua狀態,我還會回過頭來總結Lua狀態的。相信我,我還會回來的。