| 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 effilcd effil && mkdir build && cd buildcmake .. && make installluarocks 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 、 stringlua_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 ,否则为falseeffil.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 "