【那不是真的多線程】
Lua不支持真正的多線程,這句話我在《Lua中的協同程序》這篇文章中就已經說了。根據我的編程經驗,在開發過程中,如果可以避免使用線程,那就堅決不用線程,如果實在沒有更好的辦法,那就只能退而用之。為什么?首先,多個線程之間的通信比較麻煩,同時,線程之間共享內存,對於共享資源的訪問,使用都是一個不好控制的問題;其次,線程之間來回切換,也會導致一些不可預估的問題,對性能也是一種損耗。Lua不支持真正的多線程,而是一種協作式的多線程,彼此之間協作完成,並不是搶占完成任務,由於這種協作式的線程,因此可以避免由不可預知的線程切換所帶來的問題;另一方面,Lua的多個狀態之間不共享內存,這樣便為Lua中的並發操作提供了良好的基礎。
【多個線程】
從C API的角度來看,將線程想象成一個棧可能更形象些。從實現的觀點來看,一個線程的確就是一個棧。每個棧都保留着一個線程中所有未完成的函數調用信息,這些信息包括調用的函數、每個調用的參數和局部變量。也就是說,一個棧擁有一個線程得以繼續運行的所有信息。因此,多個線程就意味着多個獨立的棧。
當調用Lua C API中的大多數函數時,這些函數都作用於某個特定的棧。當我們調用lua_pushnumber時,就會將數字壓入一個棧中,那么Lua是如何知道該使用哪個棧的呢?答案就在類型lua_State中。這些C API的第一個參數不僅表示了一個Lua狀態,還表示了一個記錄在該狀態中的線程。
只要創建一個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對象一樣都是垃圾回收的對象。當新建一個線程時,線程會壓入棧,這樣能確保新線程不會成為垃圾,而有的時候,你在處理棧中數據時,不經意間就把線程彈出棧了,而當你再次使用該線程時,可能導致找不到對應的線程而程序崩潰。為了避免這種情況的發生,可以保持一個對線程的引用,比如在注冊表中保存一個對線程的引用。
當擁有了一個線程以后,我們就可以像主線程那樣來使用它,以前博文中提到的對棧的操作,對這個新的線程都適用。然而,使用多線程的目的不是為了實現這些簡單的功能,而是為了實現協同程序。
為了掛起某些協同程序的執行,並在稍后恢復執行,我們可以使用lua_resume函數來實現。
int lua_resume(lua_State *L, int narg);
lua_resume可以啟動一個協同程序,它的用法就像lua_call一樣。將待調用的函數壓入棧中,並壓入其參數,最后在調用lua_resume時傳入參數的數量narg。這個行為與lua_pcall類似,但有3點不同。
- lua_resume沒有參數用於指出期望的結果數量,它總是返回被調用函數的所有結果;
- 它沒有用於指定錯誤處理函數的參數,發生錯誤時不會展開棧,這就可以在發生錯誤后檢查棧中的情況;
- 如果正在運行的函數交出(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狀態的。相信我,我還會回來的。