Invoking interpreter states from multiple threads for the Lua scripting language.
This package provides a way to create new Lua states from within Lua for using them in arbitrary threads. The implementation is independent from the underlying threading library (e.g. Lanes or lua-llthreads2).
The general principle is to prepare a state by running a setup function within this state that returns a callback function which afterwards can be called from different threads. This can be useful in a thread-pool scenario when the number of states exceeds the number of available threads.
This package is also available via LuaRocks, see https://luarocks.org/modules/osch/mtstates.
See below for full reference documentation.
- Tested operating systems: Linux, Windows, MacOS
- Other Unix variants: could work, but untested, required are:
- gcc atomic builtins or C11
stdatomic.h
pthread.h
or C11threads.h
- gcc atomic builtins or C11
- Tested Lua versions: 5.1, 5.2, 5.3, 5.4, luajit 2.0 & 2.1
For the examples llthreads2 is used as low level multi-threading implementation.
First example: a state is constructed whose callback function is called from a parallel thread and from the main thread. The state is passed by integer id to the created thread.
local llthreads = require("llthreads2.ex")
local mtstates = require("mtstates")
local state = mtstates.newstate(function(init)
local list = { init }
local cmds = {
add = function(arg) list[#list + 1] = arg end,
get = function() return table.concat(list, " ") end
}
return function(cmd, ...)
return cmds[cmd](...)
end
end,
"Hello")
local thread = llthreads.new(function(stateId)
local mtstates = require("mtstates")
local state = mtstates.state(stateId)
return state:call("add", "World")
end,
state:id())
thread:start()
thread:join()
assert("Hello World" == state:call("get"))
It's not possible for concurrently runnings threads to call a state's callback function simultaneously, therefore each call waits until the previous call is finished. It is possible to give a timeout for calling a state callback (see state:tcall()).
The following example uses mtmsg to synchronize access to a state:
local llthreads = require("llthreads2.ex")
local mtmsg = require("mtmsg")
local mtstates = require("mtstates")
local state = mtstates.newstate(function()
local list = {}
local cmds = {
add = function(arg) list[#list + 1] = arg end,
get = function() return table.concat(list, " ") end
}
return function(cmd, ...)
return cmds[cmd](...)
end
end)
local threadIn = mtmsg.newbuffer()
local threadOut = mtmsg.newbuffer()
local thread = llthreads.new(function(threadInId, threadOutId)
local mtstates = require("mtstates")
local mtmsg = require("mtmsg")
local threadIn = mtmsg.buffer(threadInId)
local threadOut = mtmsg.buffer(threadOutId)
while true do
local cmd, stateId, arg = threadIn:nextmsg()
if cmd == "finish" then
threadOut:addmsg("finished")
break
else
local state = mtstates.state(stateId)
state:call(cmd, arg)
threadOut:addmsg("done")
end
end
end,
threadIn:id(), threadOut:id())
thread:start()
threadIn:addmsg("add", state:id(), "Hello")
threadIn:addmsg("add", state:id(), "World")
assert(threadOut:nextmsg() == "done")
assert(threadOut:nextmsg() == "done")
assert("Hello World" == state:call("get"))
threadIn:addmsg("finish")
assert(threadOut:nextmsg() == "finished")
assert(thread:join())
- Module Functions
- mtstates.newstate()
- mtstates.state()
- mtstates.singleton()
- mtstates.id()
- mtstates.type()
- State Methods
- state:id()
- state:name()
- state:call()
- state:tcall()
- state:interrupt()
- state:isowner()
- state:close()
- Errors
- mtstates.error.ambiguous_name
- mtstates.error.concurrent_access
- mtstates.error.interrupted
- mtstates.error.invoking_state
- mtstates.error.object_closed
- mtstates.error.out_of_memory
- mtstates.error.state_result
- mtstates.error.unknown_object
-
mtstates.newstate([name,][libs,]setup[,...)
Creates a new state. The given setup function is executed in the new state. The setup function must return a state callback function which can be called using the method state:call(). Additional values that are returned from the setup function are given back as additional results by mtstates.newstate().
- name - optional string, the name of the new state, can be nil or omitted to create a state without name.
- libs - optional boolean, if true all standard libraries are opened in the new state, if false only the basic lua functions and the module "package" are loaded, other standard libraries are preloaded and can be loaded by require, defaults to true,
- setup - state setup function, can be a function without upvalues or a string containing lua code. The setup function must return a function that is used as state callback function for the new created state.
- ... - additional parameters, are transfered to the new state and are given as arguments to the setup function. Arguments can be simple data types (string, number, boolean, nil, light user data) or carray objects.
This function returns a state referencing lua object with state:isowner() == true.
State objects implement the Notify C API, see src/notify_capi.h, i.e. the state object has an an associated meta table entry _capi_notify delivered by the C API function notify_get_capi() and the associated C API function toNotifier() returns a valid pointer for a given state object. The notify will invoke the state's callback function without arguments. See also example06.lua.
State objects also implement the Receiver C API, i.e. native code can pass arguments to the state's callback function from any thread.
Possible errors: mtstates.error.invoking_state, mtstates.error.state_result
-
mtstates.state(id|name)
Creates a lua object for referencing an existing state. The state must be referenced by its id or name. Referencing the state by id is much faster than referencing by name if the number of states increases.
-
id - integer, the unique state id that can be obtained by state:id().
-
name - string, the optional name that was given when the state was created with mtstates.newstate(). To find a state by name the name must be unique for the whole process.
This function returns a state referencing lua object with state:isowner() == false.
Possible errors: mtstates.error.ambiguous_name, mtstates.error.unknown_object
-
-
mtstates.singleton(name,[libs,]setup[,...)
Creates a lua object for referencing an existing state by name. If the state referenced by the name does not exist, a new state is created using the supplied parameters. This invocation can be used to atomically construct a globally named state securely from different threads.
If mtstates.singleton() is called with the same name parameter from concurrently running threads the second invocation waits until the first invocation is finished or fails.
-
name - mandatory string, name for finding an existing state. If the state is not found the following parameters are used to create a new state with the given name.
-
libs, setup, ... - same parameters as in mtstates.newstate().
This function returns a state referencing lua object with state:isowner() == true.
This function should only by used for very special use cases: Because references to the same mtstates's state from different lua states are reference counted special care has to be taken that no reference cycle is constructed using mtstates.singleton().
Possible errors: mtstates.error.ambiguous_name, mtstates.error.invoking_state, mtstates.error.state_result
-
-
mtstates.id()
Gives the state id of the currently running state invoking this function. Returns
nil
if the current state was not constructed via mtstates.newstate() or mtstates.singleton(). -
mtstates.type(arg)
Returns the type of arg as string. Same as type(arg) for builtin types. For userdata objects it tries to determine the type from the __name field in the metatable and checks if the metatable can be found in the lua registry for this key as created by luaL_newmetatable.
Returns "userdata" for userdata where the __name field in the metatable is missing or does not have a corresponding entry in the lua registry.
Returns "mtstates.state" if the arg is the state userdata type provided by this package.
-
state:id()
Returns the state's id as integer. This id is unique among all states for the whole process.
-
state:name()
Returns the state's name that was given to mtstates.newstate().
-
state:call(...)
Invokes the state callback function that was returned by the state setup function when the state was created with mtstates.newstate().
- ... - All argument parameters are transfered to the state and given to the state callback function. Arguments can be simple data types (string, number, boolean, nil, light user data) or carray objects.
If the state callback function is processed in a concurrently running thread the state:call() method waits for the other call to complete before the state callback function is invoked. See next method state:tcall() for calling a state with timeout parameter.
Returns the results of the state callback function. Results can be simple data types (string, number, boolean, nil, light user data) or carray objects.
Possible errors: mtstates.error.interrupted, mtstates.error.invoking_state, mtstates.error.object_closed, mtstates.error.state_result
-
state:tcall(timeout, ...)
Invokes the state callback function that was returned by the state setup function when the state was created with mtstates.newstate().
-
timeout float, maximal time in seconds for waiting for a concurrently running call of the state callback function to complete.
-
... - additional argument parameters are transfered to the state and given to the state callback function. Arguments can be simple data types (string, number, boolean, nil, light user data) or carray objects.
If the state could be accessed within the timeout state:tcall() returns the boolean value true and all results from the state callback function. Results can be simple data types (string, number, boolean, nil, light user data) or carray objects.
Returns false if the state could not be accessed during the timeout.
Possible errors: mtstates.error.interrupted, mtstates.error.invoking_state, mtstates.error.object_closed, mtstates.error.state_result
-
-
state:interrupt([flag])
Interrupts the state by installing a debug hook that triggers an error mtstates.error.interrupted. Therefore the interrupt only occurs when lua code is invoked. If the state is within a c function the interrupt occurs when the c function returns.
- flag - optional boolean, if not specified or nil the state is only interrupted once, if true the state is interrupted at every operation again, if false the state is no longer interrupted.
-
state:isowner()
Returns
true
if the state referencing lua object owns the referenced state. If the last owning lua object is garbage collected the underlying state is closed.State referencing objects constructed via mtstates.newstate() or mtstates.singleton() are owning the state.
State referencing objects constructed via mtstates.state() are not owning the state.
-
state:close()
Closes the underlying state and frees the memory. Every operation from any referencing object raises a mtstates.error.object_closed.
A closed state cannot be found via its name or id using the function mtstates.state().
A closed state cannot be reactivated.
Possible errors: mtstates.error.concurrent_access
-
All errors raised by this module are string values. Special error strings are available in the table
mtstates.error
, example:local mtstates = require("mtstates") assert(mtstates.error.state_result == "mtstates.error.state_result")
These can be used for error evaluation purposes, example:
local mtstates = require("mtstates") local _, err = pcall(function() mtstates.newstate(function() end) end) assert(err:match(mtstates.error.state_result))
-
mtstates.error.ambiguous_name
More than one state was found for the given name to mtstates.state(). To find a state by name, the state name must be unique among all states in the whole process
-
mtstates.error.concurrent_access
Raised if state:close() is called while the state is processing a call on a parallel running thread.
-
mtstates.error.interrupted
The state was interrupted by invoking the method state:interrupt().
-
mtstates.error.invoking_state
An error ocurred when running code within another state, e.g. when the setup function given to mtstates.newstate() is executed during state creation or when the state callback function is called in state:call().
This error contains the traceback from within the state and the outer traceback from context that called mtstates.newstate() or state:call().
-
mtstates.error.object_closed
An operation is performed on a closed state, i.e. the method state:close() has been called.
-
mtstates.error.out_of_memory
State memory cannot be allocated.
-
mtstates.error.state_result
A result value from a function invoked in another state is invalid, e.g. the setup function given to mtstates.newstate() returns no state callback or a state callback or state setup function returns a value that cannot be transferred back to the invoking state.
-
mtstates.error.unknown_object
A reference to an existing state (via mtstates.state()) cannot be created because the object cannot be found by the given id or name.
All mtstates objects are subject to garbage collection and therefore a owning reference to a created object is needed to keep it alive, example:
local mtstates = require("mtstates") local s1 = mtstates.newstate("return function() return 3 end") local id = s1:id() local s2 = mtstates.state(id) local s3 = mtstates.state(id) assert(s1:isowner() == true) assert(s2:isowner() == false) assert(s3:isowner() == false) s2 = nil collectgarbage() assert(s3:call() == 3) assert(mtstates.state(id):id() == id) s1 = nil collectgarbage() local _, err = pcall(function() s3:call() end) assert(err:match(mtstates.error.object_closed)) local _, err = pcall(function() mtstates.state(id) end) assert(err:match(mtstates.error.unknown_object))
End of document.