Linux + MacOS | 視窗 |
---|---|
Effil 是 Lua 的多執行緒函式庫。它允許生成本機線程和安全的資料交換。 Effil 旨在為 lua 開發人員提供清晰、簡單的 API。
Effil 支援 lua 5.1、5.2、5.3 和 LuaJIT。需要 C++14 編譯器合規性。使用 GCC 4.9+、clang 3.8 和 Visual Studio 2015 進行測試。
git clone --recursive https://github.com/effil/effil effil
cd effil && mkdir build && cd build
cmake .. && make install
luarocks install effil
您可能知道,真正支援多執行緒的腳本語言並不多(Lua/Python/Ruby 等具有全域解釋器鎖,又稱 GIL)。 Effil 透過在單獨的本機執行緒中執行獨立的 Lua VM 實例來解決這個問題,並為建立執行緒和資料共享提供強大的通訊原語。
Effil 函式庫提供了三個主要抽象:
effil.thread
- 提供用於執行緒管理的 API。effil.table
- 提供用於表格管理的 API。表可以在線程之間共享。effil.channel
- 為順序資料交換提供先進先出容器。還有一堆用於處理線程和表的實用程式。
local effil = require ( " effil " )
function bark ( name )
print ( name .. " barks from another thread! " )
end
-- run funtion bark in separate thread with name "Spaky"
local thr = effil . thread ( bark )( " Sparky " )
-- wait for completion
thr : wait ()
輸出: Sparky barks from another thread!
local effil = require ( " effil " )
-- channel allow to push data in one thread and pop in other
local channel = effil . channel ()
-- writes some numbers to channel
local function producer ( channel )
for i = 1 , 5 do
print ( " push " .. i )
channel : push ( i )
end
channel : push ( nil )
end
-- read numbers from channels
local function consumer ( channel )
local i = channel : pop ()
while i do
print ( " pop " .. i )
i = channel : pop ()
end
end
-- run producer
local thr = effil . thread ( producer )( channel )
-- run consumer
consumer ( channel )
thr : wait ()
輸出:
push 1
push 2
pop 1
pop 2
push 3
push 4
push 5
pop 3
pop 4
pop 5
effil = require ( " effil " )
-- effil.table transfers data between threads
-- and behaves like regualr lua table
local storage = effil . table { string_field = " first value " }
storage . numeric_field = 100500
storage . function_field = function ( a , b ) return a + b end
storage . table_field = { fist = 1 , second = 2 }
function check_shared_table ( storage )
print ( storage . string_field )
print ( storage . numeric_field )
print ( storage . table_field . first )
print ( storage . table_field . second )
return storage . function_field ( 1 , 2 )
end
local thr = effil . thread ( check_shared_table )( storage )
local ret = thr : get ()
print ( " Thread result: " .. ret )
輸出:
first value
100500
1
2
Thread result: 3
Effil 允許使用effil.channel
、 effil.table
或直接作為effil.thread
的參數在執行緒之間傳輸資料(Lua 解譯器狀態)。
nil
、 boolean
、 number
、 string
lua_dump
轉儲函數。根據規則捕獲上值。lua_iscfunction
傳回 true)僅透過使用lua_tocfunction
(在原始 lua_State 中)和 lua_pushcfunction(在新 lua_State 中)的指標來傳送。lua_iscfunction
傳回 true,但lua_tocfunction
傳回 nullptr。因此,我們找不到在 lua_States 之間傳輸它的方法。effil.table
。因此,任何 Lua 表都會變成effil.table
。對於大表,表序列化可能會花費大量時間。因此,最好將資料直接放入effil.table
以避免表序列化。讓我們考慮兩個例子: -- Example #1
t = {}
for i = 1 , 100 do
t [ i ] = i
end
shared_table = effil . table ( t )
-- Example #2
t = effil . table ()
for i = 1 , 100 do
t [ i ] = i
end
在範例 #1 中,我們建立常規表,填入它並將其轉換為effil.table
。在這種情況下,Effil 需要再次檢查所有表格欄位。另一個方法是範例 #2,我們先建立effil.table
,然後將資料直接放入effil.table
。第二種方法要快得多,試著遵循這個原則。
所有使用時間指標的操作都可以是阻塞或非阻塞的,並使用以下 API: (time, metric)
,其中metric
是時間間隔,如's'
(秒),而time
是間隔數。
例子:
thread:get()
- 無限等待線程完成。thread:get(0)
- 非阻塞獲取,只需檢查線程是否完成並返回thread:get(50, "ms")
- 阻塞等待 50 毫秒。可用時間間隔清單:
ms
毫秒;s
- 秒(預設);m
分鐘;h
小時。所有阻塞操作(即使在非阻塞模式下)都是中斷點。在此類操作中掛起的執行緒可以透過呼叫 thread:cancel() 方法來中斷。
local effil = require " effil "
local worker = effil . thread ( function ()
effil . sleep ( 999 ) -- worker will hang for 999 seconds
end )()
worker : cancel ( 1 ) -- returns true, cause blocking operation was interrupted and thread was cancelled
使用函數 Effil 使用lua_dump
和lua_load
方法對它們進行序列化和反序列化。所有函數的上值都依照與平常相同的規則儲存。如果函數具有不支援類型的 upvalue,則函數無法傳輸到 Effil。在這種情況下你會得到錯誤。
使用函數 Effil 也可以儲存函數環境( _ENV
)。將環境視為常規表 Effil 將以與任何其他表相同的方式儲存它。但儲存全域_G
沒有意義,所以有一些具體的:
_ENV ~= _G
)時,Effil 才會序列化並儲存函數環境。 可以使用執行緒物件thread:cancel()
和thread:pause()
的對應方法來暫停和取消effil.thread
。
您嘗試中斷的執行緒可以在兩個執行點中斷:明確和隱式。
顯式的點是effil.yield()
local thread = effil . thread ( function ()
while true do
effil . yield ()
end
-- will never reach this line
end )()
thread : cancel ()
隱式點是使用 lua_sethook 和 LUA_MASKCOUNT 設定的 lua 偵錯鉤子呼叫。
隱式點是可選的,並且僅當 thread_runner.step > 0 時才啟用。
local thread_runner = effil . thread ( function ()
while true do
end
-- will never reach this line
end )
thread_runner . step = 10
thread = thread_runner ()
thread : cancel ()
此外,執行緒可以在任何阻塞或非阻塞等待操作中取消(但不能暫停)。
local channel = effil . channel ()
local thread = effil . thread ( function ()
channel : pop () -- thread hangs waiting infinitely
-- will never reach this line
end )()
thread : cancel ()
取消是如何進行的?
當您取消線程時,當它到達任何中斷點時,它會產生 lua error
並顯示訊息"Effil: thread is cancelled"
。這意味著您可以使用pcall
捕獲此錯誤,但線程將在下一個中斷點產生新的錯誤。
如果您想捕獲自己的錯誤但傳遞取消錯誤,您可以使用 efil.pcall()。
只有在取消錯誤完成時,已取消執行緒的狀態才會等於cancelled
。這表示如果您捕獲取消錯誤,則執行緒可能會以completed
狀態或failed
狀態結束(如果還會出現其他錯誤)。
effil.thread
是創建執行緒的方式。執行緒可以停止、暫停、恢復和取消。所有線程操作都可以是同步的(帶有可選的超時)或非同步的。每個線程都以自己的 lua 狀態運行。
使用effil.table
和effil.channel
透過執行緒傳輸資料。請參閱此處的線程使用範例。
runner = effil.thread(func)
創建線程運行器。 Runner 為每次呼叫產生新執行緒。
輸入: func - Lua 函數
輸出: runner - 用於設定和運行新執行緒的執行緒運行程式對象
允許配置和運行新執行緒。
thread = runner(...)
在單獨的線程中運行具有指定參數的捕獲函數並返回線程句柄。
input :捕獲的函數所需的任意數量的參數。
輸出:線程句柄物件。
runner.path
是新狀態的 Lua package.path
值。預設值繼承父狀態的package.path
。
runner.cpath
是新狀態的 Lua package.cpath
值。預設值從父狀態繼承package.cpath
。
runner.step
取消點(執行緒可以停止或暫停)之間的 lua 指令數 lua。預設值為 200。
線程句柄提供了與線程互動的API。
status, err, stacktrace = thread:status()
返回線程狀態。
輸出:
status
- 字串值描述執行緒的狀態。可能的值為: "running", "paused", "cancelled", "completed" and "failed"
。err
- 錯誤訊息(如果有)。僅當執行緒狀態 == "failed"
時才指定該值。stacktrace
- 失敗執行緒的堆疊追蹤。僅當執行緒狀態 == "failed"
時才指定該值。... = thread:get(time, metric)
等待線程完成並傳回函數結果,如果出現錯誤則不傳回任何內容。
輸入:以時間指標表示的操作逾時
輸出:捕獲的函數呼叫的結果,或在發生錯誤時什麼都沒有。
thread:wait(time, metric)
等待線程完成並返回線程狀態。
輸入:以時間指標表示的操作逾時
輸出:傳回線程的狀態。輸出與thread:status()
相同
thread:cancel(time, metric)
中斷線程執行。一旦呼叫該函數,就會設定“取消”標誌,並且線程可以在將來的某個時候停止(即使在該函數呼叫完成之後)。為了確保線程停止,請以無限超時呼叫此函數。取消已完成的執行緒不會執行任何操作並傳回true
。
輸入:以時間指標表示的操作逾時
輸出:如果執行緒停止則傳回true
,否則回傳false
。
thread:pause(time, metric)
暫停線程。一旦呼叫該函數,就會設定「暫停」標誌,並且執行緒可以在將來的某個時候暫停(即使在該函數呼叫完成之後)。為了確保線程暫停,請無限超時呼叫此函數。
輸入:以時間指標表示的操作逾時
輸出:如果線程暫停則回傳true
,否則回傳false
。如果線程完成函數將傳回false
thread:resume()
恢復暫停的執行緒。如果線程暫停,函數會立即恢復線程。該函數對於已完成的執行緒不執行任何操作。函數沒有輸入和輸出參數。
id = effil.thread_id()
給出唯一識別符。
輸出:傳回目前執行緒的唯一字串id
。
effil.yield()
明確取消點。函數檢查目前執行緒的取消或暫停標誌,如果需要,它會執行相應的操作(取消或暫停執行緒)。
effil.sleep(time, metric)
掛起當前線程。
輸入:時間指標參數。
effil.hardware_threads()
傳回實作支援的並發線程數。基本上從 std::thread::hardware_concurrency 轉送值。
輸出:並發硬體線程數。
status, ... = effil.pcall(func, ...)
工作方式與標準 pcall 完全相同,只是它不會捕獲由 thread:cancel() 呼叫引起的執行緒取消錯誤。
輸入:
輸出:
true
,否則為false
effil.table
是一種在 efil 執行緒之間交換資料的方法。它的行為幾乎和標準 lua 表一樣。所有與共享表相關的操作都是線程安全的。共用表格儲存原始類型(數字、布林值、字串)、函數、表格、輕型使用者資料和基於 efil 的使用者資料。共享表不儲存lua 執行緒(協程)或任意使用者資料。請參閱此處的共享表使用範例
將共享表與常規表一起使用。如果你想將常規表儲存在共用表中,efil 會隱式地將原始表轉儲到新的共用表中。共享表始終將子表儲存為共享表。
使用帶有函數的共享表。如果將函數儲存在共用表中,efil 會隱式轉儲該函數並將其儲存為字串(並且是 upvalues)。所有函數的上值將根據以下規則捕獲。
table = effil.table(tbl)
建立新的空共享表。
input : tbl
- 是可選參數,它只能是常規 Lua 表,其條目將複製到共用表。
輸出:空共享表的新實例。它可以為空也可以不為空,這取決於tbl
內容。
table[key] = value
使用指定值設定表的新鍵。
輸入:
key
- 支援類型的任何值。查看支援的類型列表value
- 支援類型的任何值。查看支援的類型列表value = table[key]
從具有指定鍵的表中取得值。
input : key
- 支援類型的任何值。查看支援的類型列表
輸出: value
- 支援類型的任何值。查看支援的類型列表
tbl = effil.setmetatable(tbl, mtbl)
將新的元表設定為共享表。與標準 setmetatable 類似。
輸入:
tbl
應該是要為其設定元表的共用表。mtbl
應該是常規表或共用表,它將成為元表。如果它是常規表efil將建立一個新的共用表並複製mtbl
的所有欄位。將mtbl
設定為nil
以從共用表中刪除元表。輸出:僅傳回帶有新元表值的tbl
,類似於標準 Lua setmetatable方法。
mtbl = effil.getmetatable(tbl)
返回當前元表。類似標準 getmetatable
輸入: tbl
應該是共享表。
輸出:傳回指定共享表的元表。傳回的表始終具有effil.table
類型。預設元表是nil
。
tbl = effil.rawset(tbl, key, value)
設定表格項目而不呼叫元方法__newindex
。類似於標準原始集
輸入:
tbl
是共享表。key
- 要覆蓋的表的鍵。密鑰可以是任何受支援的類型。value
- 要設定的值。該值可以是任何受支援的類型。輸出:傳回相同的共享表tbl
value = effil.rawget(tbl, key)
取得表值而不呼叫元方法__index
。類似於標準 rawget
輸入:
tbl
是共享表。key
- 用於接收特定值的表的鍵。密鑰可以是任何受支援的類型。輸出:傳回指定key
下儲存的所需value
effil.G
是一個全域預定義的共享表。該表始終存在於任何線程(任何 Lua 狀態)中。
effil = require " effil "
function job ()
effil = require " effil "
effil . G . key = " value "
end
effil . thread ( job )(): wait ()
print ( effil . G . key ) -- will print "value"
result = effil.dump(obj)
將effil.table
轉換為常規 Lua 表。
tbl = effil . table ({})
effil . type ( tbl ) -- 'effil.table'
effil . type ( effil . dump ( tbl )) -- 'table'
effil.channel
是一種在 efil 執行緒之間順序交換資料的方法。它允許從一個線程推送訊息並從另一個線程彈出訊息。通道的訊息是一組支援類型的值。所有通道操作都是線程安全的。請參閱此處的通道使用範例
channel = effil.channel(capacity)
建立一個新頻道。
input :通道的可選容量。如果capacity
等於0
或nil
,則通道大小是無限的。預設容量為0
。
輸出:傳回通道的新實例。
pushed = channel:push(...)
將訊息推送到頻道。
input :任意數量的支援類型的值。多個值被視為單一通道訊息,因此一次推送到通道會使容量減少一。
輸出:如果 value(-s) 適合通道容量,則pushed
等於true
,否則為false
。
... = channel:pop(time, metric)
從頻道彈出訊息。從通道中刪除值(-s)並傳回它們。如果通道為空,則等待任何值出現。
輸入:以時間指標表示的等待逾時(僅在通道為空時使用)。
輸出:由單一通道:push() 呼叫推送的可變數量的值。
size = channel:size()
取得頻道中的實際訊息量。
輸出:通道中的消息量。
Effil 為effil.table
和effil.channel
(以及具有捕獲的上值的函數)提供自訂垃圾收集器。它允許安全管理多個線程中表和通道的循環引用。但它可能會導致額外的記憶體使用。 effil.gc
提供了一組配置 efil 垃圾收集器的方法。但是,通常您不需要配置它。
當 efil 建立新的共享物件(具有捕獲的上值的表、通道和函數)時,垃圾收集器就會執行其工作。每次迭代 GC 都會檢查物件的數量。如果分配的物件數量變得更高,則特定閾值 GC 開始垃圾收集。閾值的計算方式為previous_count * step
,其中previous_count
- 上一次迭代的物件數量(預設為100 ), step
是使用者指定的數值係數(預設為2.0 )。
例如:如果 GC step
為2.0
且分配的物件數量為120
(上次 GC 迭代後剩餘的),那麼當分配的物件數量等於240
時,GC 將開始收集垃圾。
每個線程都表示為具有自己的垃圾收集器的單獨 Lua 狀態。因此,對象最終將被刪除。 Effil 物件本身也由 GC 管理,並使用__gc
userdata 元方法作為反序列化器掛鉤。強制刪除物件:
collectgarbage()
。effil.gc.collect()
。effil.gc.collect()
強制垃圾回收,但它並不能保證刪除所有 efil 物件。
count = effil.gc.count()
顯示分配的共享表和通道的數量。
輸出:傳回目前指派的物件數。最小值為 1, effil.G
總是存在。
old_value = effil.gc.step(new_value)
取得/設定 GC 記憶體步進倍增器。預設值為2.0
。當分配的物件數量step
長增長時,GC 會觸發收集。
input : new_value
是要設定的步驟的可選值。如果它是nil
那麼函數將只傳回當前值。
輸出: old_value
是步驟的目前值(如果new_value == nil
)或先前值(如果new_value ~= nil
)。
effil.gc.pause()
暫停GC。垃圾收集不會自動執行。函數沒有任何輸入或輸出
effil.gc.resume()
恢復GC。啟用自動垃圾收集。
enabled = effil.gc.enabled()
獲取GC狀態。
輸出:如果啟用自動垃圾收集,則傳回true
,否則傳回false
。預設情況下傳回true
。
size = effil.size(obj)
傳回 Effil 物件中的條目數。
輸入: obj
是共用表或通道。
輸出:共享表中的條目數或通道中的消息數
type = effil.type(obj)
線程、通道和表格都是使用者資料。因此, type()
將傳回任何類型的userdata
。如果您想更精確地檢測類型,請使用effil.type
。它的行為類似於常規type()
,但它可以檢測有效的特定使用者資料。
輸入: obj
是任何類型的物件。
輸出:類型的字串名稱。如果obj
是 Effil 對象,則函數傳回類似effil.table
的字串,在其他情況下,它會傳回 lua_typename 函數的結果。
effil . type ( effil . thread ()) == " effil.thread "
effil . type ( effil . table ()) == " effil.table "
effil . type ( effil . channel ()) == " effil.channel "
effil . type ({}) == " table "
effil . type ( 1 ) == " number "