Effil is a multithreading library for Lua.
It allows to spawn native threads and safe data exchange.
Effil has been designed to provide clear and simple API for lua developers.
Effil supports lua 5.1, 5.2, 5.3 and LuaJIT.
Requires C++14 compiler compliance. Tested with GCC 4.9+, clang 3.8 and Visual Studio 2015.
As you may know there are not much script languages with real multithreading support (Lua/Python/Ruby and etc has global interpreter lock aka GIL). Effil solves this problem by running independent Lua VM instances in separate native threads and provides robust communicating primitives for creating threads and data sharing.
Effil library provides three major abstractions:
effil.thread - provides API for threads management.
effil.table - provides API for tables management. Tables can be shared between threads.
effil.channel - provides First-In-First-Out container for sequential data exchange.
And bunch of utilities to handle threads and tables as well.
Examples
Spawn the thread
local effil =require("effil")
functionbark(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()
Output:Sparky barks from another thread!
Sharing data with effil.channel
local effil =require("effil")
-- channel allow to push data in one thread and pop in otherlocal channel = effil.channel()
-- writes some numbers to channellocalfunctionproducer(channel)
for i =1, 5doprint("push ".. i)
channel:push(i)
end
channel:push(nil)
end-- read numbers from channelslocalfunctionconsumer(channel)
local i = channel:pop()
while i doprint("pop ".. i)
i = channel:pop()
endend-- run producerlocal thr = effil.thread(producer)(channel)
-- run consumerconsumer(channel)
thr:wait()
Output:
push 1
push 2
pop 1
pop 2
push 3
push 4
push 5
pop 3
pop 4
pop 5
Sharing data with effil.table
effil =require("effil")
-- effil.table transfers data between threads-- and behaves like regualr lua tablelocal 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 }
functioncheck_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)
endlocal thr = effil.thread(check_shared_table)(storage)
local ret = thr:get()
print("Thread result: ".. ret)
Output:
first value
100500
1
2
Thread result: 3
Important notes
Effil allows to transmit data between threads (Lua interpreter states) using effil.channel, effil.table or directly as parameters of effil.thread.
Primitive types are transmitted 'as is' by copy: nil, boolean, number, string
Functions are dumped using lua_dump. Upvalues are captured according to the rules.
C functions (for which lua_iscfunction returns true) are transmitted just by a pointer using lua_tocfunction (in original lua_State) and lua_pushcfunction (in new lua_State). caution: in LuaJIT standard functions like tonumber are not real C function, so lua_iscfunction returns true but lua_tocfunction returns nullptr. Due to that we don't find way to transmit it between lua_States.
Userdata and Lua threads (coroutines) are not supported.
Tables are serialized to effil.table recursively. So, any Lua table becomes effil.table. Table serialization may take a lot of time for big table. Thus, it's better to put data directly to effil.table avoiding a table serialization. Let's consider 2 examples:
-- Example #1
t = {}
for i =1, 100do
t[i] = i
end
shared_table = effil.table(t)
-- Example #2
t = effil.table()
for i =1, 100do
t[i] = i
end
In the example #1 we create regular table, fill it and convert it to effil.table. In this case Effil needs to go through all table fields one more time. Another way is example #2 where we firstly created effil.table and after that we put data directly to effil.table. The 2nd way pretty much faster try to follow this principle.
Blocking and nonblocking operations:
All operations which use time metrics can be blocking or non blocking and use following API:
(time, metric) where metric is time interval like 's' (seconds) and time is a number of intervals.
Example:
thread:get() - infinitely wait for thread completion.
thread:get(0) - non blocking get, just check is thread finished and return
thread:get(50, "ms") - blocking wait for 50 milliseconds.
List of available time intervals:
ms - milliseconds;
s - seconds (default);
m - minutes;
h - hours.
All blocking operations (even in non blocking mode) are interruption points. Thread hanged in such operation can be interrupted by invoking thread:cancel() method.
Example
local effil =require"effil"local worker = effil.thread(function()
effil.sleep(999) -- worker will hang for 999 secondsend)()
worker:cancel(1) -- returns true, cause blocking operation was interrupted and thread was canceled
Function's upvalues
Working with functions Effil serializes and deserializes them using lua_dump and lua_load methods. All function's upvalues are stored following the same rules as usual. If function has upvalue of unsupported type this function cannot be transmitted to Effil. You will get error in that case.
Working with function Effil can store function environment (_ENV) as well. Considering environment as a regular table Effil will store it in the same way as any other table. But it does not make sence to store global _G, so there are some specific:
Lua = 5.1: function environment is not stored at all (due to limitations of lua_setfenv we cannot use userdata)
Lua > 5.1: Effil serialize and store function environment only if it's not equal to global environment (_ENV ~= _G).
API Reference
Thread
effil.thread is the way to create a thread. Threads can be stopped, paused, resumed and canceled.
All operation with threads can be synchronous (with optional timeout) or asynchronous.
Each thread runs with its own lua state.
Use effil.table and effil.channel to transmit data over threads.
See example of thread usage here.
runner = effil.thread(func)
Creates thread runner. Runner spawns new thread for each invocation.
input: func - Lua function
output: runner - thread runner object to configure and run a new thread
Thread runner
Allows to configure and run a new thread.
thread = runner(...)
Run captured function with specified arguments in separate thread and returns thread handle.
input: Any number of arguments required by captured function.
Is a Lua package.path value for new state. Default value inherits package.path form parent state.
runner.cpath
Is a Lua package.cpath value for new state. Default value inherits package.cpath form parent state.
runner.step
Number of lua instructions lua between cancelation points (where thread can be stopped or paused). Default value is 200. If this values is 0 then thread uses only explicit cancelation points.
Thread handle
Thread handle provides API for interaction with thread.
status, err, stacktrace = thread:status()
Returns thread status.
output:
status - string values describes status of thread. Possible values are: "running", "paused", "canceled", "completed" and "failed".
err - error message, if any. This value is specified only if thread status == "failed".
stacktrace - stacktrace of failed thread. This value is specified only if thread status == "failed".
... = thread:get(time, metric)
Waits for thread completion and returns function result or nothing in case of error.
output: Returns status of thread. The output is the same as thread:status()
thread:cancel(time, metric)
Interrupts thread execution. Once this function was invoked 'cancellation' flag is set and thread can be stopped sometime in the future (even after this function call done). To be sure that thread is stopped invoke this function with infinite timeout. Cancellation of finished thread will do nothing and return true.
output: Returns true if thread was stopped or false.
thread:pause(time, metric)
Pauses thread. Once this function was invoked 'pause' flag is set and thread can be paused sometime in the future (even after this function call done). To be sure that thread is paused invoke this function with infinite timeout.
output: Returns true if thread was paused or false. If the thread is completed function will return false
thread:resume()
Resumes paused thread. Function resumes thread immediately if it was paused. This function does nothing for completed thread. Function has no input and output parameters.
Thread helpers
id = effil.thread_id()
Gives unique identifier.
output: returns unique string id for current thread.
effil.yield()
Explicit cancellation point. Function checks cancellation or pausing flags of current thread and if it's required it performs corresponding actions (cancel or pause thread).
请发表评论