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。如果该值为 0,则线程仅使用显式取消点。
线程句柄提供了与线程交互的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 "