![]() ![]() ![]() ![]() ![]() |
Lua Lanes - multithreading in Lua |
Copyright © 2007-24 Asko Kauppi, Benoit Germain. All rights reserved.
Lua Lanes is published under the same MIT license as Lua 5.1, 5.2, 5.3 and 5.4.
This document was revised on 13-Dec-24, and applies to version 4.0.0.
Lua Lanes is a Lua extension library providing the possibility to run multiple Lua states in parallel. It is intended to be used for optimizing performance on multicore CPU's and to study ways to make Lua programs naturally parallel to begin with.
Lanes is included into your software by the regular require "lanes" method. No C side programming is needed; all APIs are Lua side, and most existing extension modules should work seamlessly together with the multiple lanes.
Lanes should build and run identically with either Lua 5.1 to Lua 5.4, as well as LuaJIT.
See comparison of Lua Lanes with other Lua multithreading solutions.
Lua Lanes supports the following operating systems:
The underlying threading code can be compiled either towards Win32 API or Pthreads. Unfortunately, thread prioritization under Pthreads requires OS specific tweaks and guessing undocumented behaviour. Other features should be portable to any modern platform.
Lua Lanes is implemented in C++20. It is built simply by make on the supported platforms (make-vc for Visual C++). See README for system specific details and limitations.
To install Lanes, all you need are the lanes.lua and lanes_core.so|dll files to be reachable by Lua (see LUA_PATH, LUA_CPATH). Or use Lua Rocks package management.
> luarocks search lanes ... output listing Lua Lanes is there ... > luarocks install lanes ... output ...
When Lanes is embedded, it is possible to statically initialize with
LANES_API void luaopen_lanes_embedded(lua_State* L_, lua_CFunction luaopen_lanes_); |
luaopen_lanes_embedded leaves the module table on the stack. lanes.configure() must still be called in order to use Lanes.
If _luaopen_lanes is nullptr, a default loader will simply attempt the equivalent of luaL_dofile(L, "lanes.lua").
To embed Lanes, compile source files in you application. In any Lua state were you want to use Lanes, initialize it as follows:
#include "lanes.hpp" int load_lanes_lua(lua_State* L) { // retrieve lanes.lua from wherever it is stored and return the result of its execution // trivial example 1: luaL_dofile(L, "lanes.lua"); // trivial example 2: luaL_dostring(L, bin2c_lanes_lua); } void embed_lanes(lua_State* L) { // we need base libraries for Lanes for work luaL_openlibs(L); ... // will attempt luaL_dofile(L, "lanes.lua"); luaopen_lanes_embedded(L, nullptr); lua_pop(L, 1); // another example with a custom loader luaopen_lanes_embedded(L, load_lanes_lua); lua_pop(L, 1); // a little test to make sure things work as expected luaL_dostring(L, "local lanes = require 'lanes'.configure{with_timers = false}; local l = lanes.linda()"); } |
The following sample shows how to initialize the Lanes module.
local lanes = require "lanes".configure(options|nil) |
Requiring the module follows Lua 5.2+ rules: the module is not available under the global name "lanes", but has to be accessed through require's return value.
Lanes needs "base", "string" and "table" to be initialized beforehand (plus "jit" for LuaJIT).
After lanes is required, it is recommended to call lanes.configure(), which is the only function exposed by the module at this point. Calling lanes.configure() will perform one-time initializations and make the rest of the API available.
If lanes.configure() is not called before starting to use Lanes, it will be called automatically for you with default settings.
Only the first occurence of require "lanes" must be followed by a call to lanes.configure(). From this point, a simple require "lanes" will be enough wherever you need to require lanes again.
After being called, lanes.configure() itself is replaced by another function that does nothing with the argument it receives, in case it happens to be called again.
Also, once Lanes is initialized, require() is replaced by another one that wraps it inside a mutex, both in the main state and in all created lanes. This prevents multiple thread-unsafe module initializations from several lanes to occur simultaneously. It remains to be seen whether this is actually useful or not: If a module is already threadsafe, protecting its initialization isn't useful. And if it is not, any parallel operation may crash without Lanes being able to do anything about it.
lanes.configure([opt_tbl]) |
lanes.configure accepts an optional options table as sole argument.
name | value | definition | |
---|---|---|---|
.allocator
|
nil/"protected"/function |
If nil, Lua states are created with lua_newstate() and reuse the allocator from the master state. If "protected", The default allocator obtained from lua_getallocf() in the master state is wrapped inside a critical section and used in all newly created states. If a function, this function is called prior to creating the state, with a single string argument, either "internal", "keeper" or "lane". It should return a full userdata created as follows:
|
|
.internal_allocator
|
"libc"/"allocator" |
Controls which allocator is used for Lanes internal allocations (for Keeper state, linda and lane management).
If "libc", Lanes uses realloc and free. If "allocator", Lanes uses whatever was obtained from the "allocator" setting. This option is mostly useful for embedders that want control all memory allocations, but have issues when Lanes tries to use the Lua State allocator for internal purposes (especially with LuaJIT). |
|
.keepers_gc_threshold
|
integer |
If <0, GC runs automatically. This is the default. If 0, GC runs after *every* Keeper operation. If >0, Keeper states run GC manually with lua_gc(LUA_GCCOLLECT) whenever memory usage reported by lua_gc(LUA_GCCOUNT) reaches this threshold. Check is made after every operation (see below). If memory usage remains above threshold after the GC cycle, an error is raised. |
|
.nb_user_keepers
|
integer in [0,100] |
Controls the number of "user" Keeper state used internally by linda) objects to transfer data between lanes. Default is 0. Lanes always creates at least one keeper state (of group 0 for the internal timer linda. If nb_user_keepers is 0, the other lindas you create will share this keeper by necessity. If there is more than one Keeper state (in total), linda creation must specify the group it belongs to. |
|
.on_state_create
|
nil/function |
If provided, will be called in every created Lua state right after initializing the base libraries, with a single string argument, either "lane" or "keeper". If it is a C function, a C closure will be reconstructed in the created state from the C pointer. Lanes will raise an error if the function has upvalues. If it is a Lua function, it will be transfered normally before the call. Keeper states will call it as well, but only if it is a C function (Keeper states are not able to execute any user Lua code). Typical usage is twofold:
|
|
.shutdown_timeout
|
number >= 0 |
Sets the duration in seconds Lanes will wait for graceful termination of running lanes at application shutdown. Default is 0.25. Lanes signals all lanes for cancellation with "soft", "hard", and "all" modes, in that order. Each attempt has shutdown_timeout seconds to succeed before the next one. Then there is a last chance at cleanup with lanes.finally(). If some lanes are still running after that point, shutdown will either freeze or throw. It is YOUR responsibility to cleanup properly after yourself. |
|
.strip_functions
|
nil/boolean | Controls function bytecode stripping when dumping them for lane transfer. Choose between faster copies or more debug info. Default is true. | |
.track_lanes
|
nil/boolean | Any non-nil|false value instructs Lanes keeps track of all lanes, so that lanes.threads() can list them. If false, lanes.threads() will raise an error when called. Default is false. | |
.verbose_errors
|
nil/boolean | If equal to true, Lanes will collect more information when transfering stuff across Lua states to help identify errors (with a cost). Default is false. | |
.with_timers
|
nil/boolean | If equal to false or nil, Lanes doesn't start the timer service, and the associated API will be absent from the interface (see below). Default is false. |
Once Lanes is configured, one should register with Lanes the modules exporting functions/userdata that will be transferred either during lane generation or through lindas.
Use lanes.require() for this purpose. This will call the original require(), then add the result to the lookup databases.
It is also possible to register a given module a posteriori with lanes.register(). This function will raise an error if the registered module is not a function, table, or full userdata.
Embedders can call the equivalent function lanes_register() from the C side, through lua_call() or similar.
local m = lanes.require "modname" lanes.register("modname", module) |
A full GC cycle can be triggered on all keeper states with lanes.collectgarbage(). This can force the collection of stale storage data for a collected Linda.
lanes.collectgarbage() |
It is also possible to install a function that will be called when Lanes is shutdown (that is, when the first state that required Lanes is closed).
lanes.finally(<some function>|nil) |
An error will be raised if you attempt to do this from inside a lane, or on bad arguments (non-function, or too many arguments).
Only the last registered finalizer is kept. It can be cleared by passing nil or nothing.
The finalizer is called unprotected from inside __gc metamethod of Lanes's Universe. Therefore, if your finalizer raises an error, Lua rules regarding errors in finalizers apply normally.
The installed function is called after all free-running lanes got a chance to terminate (see shutdown_timeout), but before lindas become unusable.
The finalizer receives a single argument, a bool indicating whether all Lanes are successfully terminated at that point. It is possible to inspect them with tracking.
If there are still running lanes when the finalizer returns: Lanes will throw a C++ std::logic_error if the finalizer returned "throw". Any other value will cause Lanes to freeze forever.
The following sample shows preparing a function for parallel calling, and calling it with varying arguments. Each of the two results is calculated in a separate OS thread, parallel to the calling one. Reading the results joins the threads, waiting for any results not already there.
local lanes = require "lanes" f = lanes.gen(function(n) return 2 * n end) a = f(1) b = f(2) print(a[1], b[1]) -- 2 4 |
func = lanes.gen([libs_str | opt_tbl [, ...],] lane_func) lane_h = func(...) |
The function returned by lanes.gen() is a "generator" for launching any number of lanes. They will share code, options, initial globals, but the particular arguments may vary. Only calling the generator function actually launches a lane, and provides a handle for controlling it.
Alternatively, lane_func may be a string, in which case it will be compiled in the lane. This was to be able to launch a lane with older versions of LuaJIT, which didn't not support lua_dump, used internally to transfer functions to the lane.
Lanes automatically copies upvalues over to the new lanes, so you need not wrap all the required elements into one 'wrapper' function. If lane_func uses some local values, or local functions, they will be there also in the new lanes.
libs_str
defines the standard libraries made available to the new Lua state:
nil | no standard libraries (default) | ||
"base" | _G namespace (the default function environment): print, assert, dofile, etc. | ||
"bit" | bit.* namespace (LuaJIT) | ||
"bit32" | bit32.* namespace (Lua 5.1 and 5.2) | ||
"coroutine" | coroutine.* namespace (part of base in Lua 5.1 and 5.2) | ||
"debug" | debug.* namespace | ||
"ffi" | ffi.* namespace (LuaJIT) | ||
"io" | io.* namespace | ||
"jit" | jit.* namespace (LuaJIT) | ||
"math" | math.* namespace | ||
"os" | os.* namespace | ||
"package" | package.* namespace and require | ||
"string" | string.* namespace | ||
"table" | table.* namespace | ||
"utf8" | utf8.* namespace (Lua 5.3 and above) | ||
"*" | All standard libraries (including those specific to LuaJIT and not listed above), as well as lanes_core. This must be used alone. |
Any non-nil value causes initialization of "base" and "jit" (the latter only for LuaJIT-based builds).
Library names may contain characters matching std::isalnum(c_) || c_ == '.' || c_ == '-' || c_ == '_'. Anything else is considered a valid name separator.
Trying to load a library not supported by the running Lua flavor will raise an error, unless the library name is postfixed with a '?'.
Initializing the standard libs takes a bit of time at each lane invocation. This is the main reason why "no libraries" is the default.
opt_tbl
is a collection of named options to control the way lanes are run:
name | value | definition |
---|---|---|
.globals
|
table |
Sets the globals table for the launched threads. This can be used for giving them constants. The key/value pairs of table are transfered in the lane globals after the libraries have been loaded and the modules required. The global values of different lanes are in no manner connected; modifying one will only affect the particular lane. |
.required
|
table |
Lists modules that have to be required in order to be able to transfer functions/userdata they exposed. Non-Lua functions are searched in lookup tables.
These tables are built from the modules listed here. required must be an array of strings, each one being the name of a module to be required. Each module is required with require() before the lanes function is invoked.
So, from the required module's point of view, requiring it manually from inside the lane body or having it required this way doesn't change anything. From the lane body's point of view, the only difference is that a module not creating a global won't be accessible.
Therefore, a lane body will also have to require a module manually, but this won't do anything more (see Lua's require documentation). ATTEMPTING TO TRANSFER A FUNCTION REGISTERED BY A MODULE NOT LISTED HERE WILL RAISE AN ERROR. |
.error_trace_level
|
string |
Sets the error reporting mode. One of "minimal" (the default), "basic", "extended". "minimal" yields only the location of the error. The other 2 yield a full stack trace, with different amounts of data extracted from the debug infos. See Results. |
.name
|
string | Name of the lane. If "auto", name is built from ar.short_src:ar.linedefined. Can be changed later from the inside of the lane with lane_threadname() (see below). |
.gc_cb
|
function | Callback that gets invoked when the lane is garbage collected. The function receives two arguments (the lane name and a string, either "closed" or "selfdestruct"). |
.priority
|
integer |
The priority of lanes generated in the range -3..+3 (default is 0).
These values are a mapping over the actual priority range of the underlying implementation. Implementation and dependability of priorities varies by platform. Especially Linux kernel 2.6 is not supporting priorities in user mode. A lane can also change its own thread priority dynamically with lanes.set_thread_priority(). |
.package
|
table |
Specifying it when libs_str doesn't cause the package library to be loaded will generate an error.If not specified, the created lane will receive the current values of package. Only path, cpath, preload and loaders (Lua 5.1)/searchers (Lua 5.2) are transfered. |
Each lane gets a global function lane_threadname() that it can use anytime to do both read and change the thread name. Supported debuggers are Microsoft Visual Studio (for the C side) and Decoda (for the Lua side).
Change HAVE_DECODA_SUPPORT() in lanesconf.h to enable the Decoda support, that sets a special global variable decoda_name in the lane's state.
The name is stored inside the Lua state registry so that it is available for error reporting. Changing decoda_name doesn't affect this hidden name or the OS thread name reported by MSVC.
When Lanes is initialized by the first lanes.configure() call, "main" is stored in the registry in the same fashion (but decoda_name and the OS thread name are left unchanged).
The lane also has a method lane:get_threadname() that gives access to that name from the caller side (returns "<unnamed>" if unset).
With Lua 5.4, Lanes have a __close metamethod, meaning they can be declared to-be-closed. __close calls lane:join(nil).
If a lane body pulls a C function or userdata exposed by a module required before Lanes itself (thus not through a hooked require()), the lane generator creation will raise an error. The name in the message is a path where it was found by scanning _G and the registry. As a utility, the name guessing functionality is exposed as such:
"type", "name" = lanes.nameof(o) |
It is possible to have the lane body run inside the lane as a coroutine. For this, just use lanes.coro() instead of lanes.gen().
local lane_h = lanes.coro(...)(...) |
local yielded_values = lane_h:resume(reply_values...) |
The lane handles are allowed to be 'let loose'; in other words you may execute a lane simply by:
lanes.gen(function(params) ... end ) (...) |
lanes.set_thread_priority(prio) |
Besides setting a default priority in the generator settings, each thread can change its own priority at will. This is also true for the main Lua state.
The priority must be in the range [-3,+3].
lanes.set_thread_affinity(affinity) |
Each thread can change its own affinity at will. This is also true for the main Lua state.
str = lane_h.error_trace_level |
Read back the value set during lane generation, one of "basic", "minimal", "extended".
str = lane_h.status |
The current execution state of a lane can be read via its status member, providing one of these values:
"pending" | Not started yet. Shouldn't stay very long in that state. | ||
"running" | Running, not suspended on a linda call, or yielded on a coroutine.yield() call. | ||
"suspended" | Coroutine lane stopped at a coroutine.yield() point. | ||
"resuming" | Parent state called lane_h:resume() on a suspended coroutine lane, but the lane hasn't yet actually resumed execution. | ||
"waiting" | Waiting at a linda :receive() or :send() | ||
"done" | Finished executing (results are ready) | ||
"error" | Met an error (reading results will propagate it) | ||
"cancelled" | Received cancellation and finished itself. |
This is similar to coroutine.status, which has: "running" / "suspended" / "normal" / "dead". Not using the exact same names is intentional.
{name = "name", status = "status", ...}|nil = lanes.threads() |
Only available if lane tracking is enabled by setting track_lanes.
Returns an array table where each entry is a table containing a lane's name and status. Returns nil if no lane is running.
Error reporting level is set in the lane generator settings.
A lane can be waited upon by simply reading its results. This can be done in two ways.
[val]= lane_h[1] |
Makes sure lane has finished or yielded, and gives its first (maybe only) return value. Other return values will be available in other lane_h indices.
If the lane ended in an error, it is propagated to master state at this place.
[...]|[nil,err,stack_tbl]= lane_h:join([timeout]) |
Waits until the lane finishes/yields, or timeout seconds have passed (forever if nil).
Unlike in reading the results in table fashion, errors are not propagated.
Possible return values are:
local lanes = require "lanes" f = lanes.gen(function() error "!!!" end) a = f(1) --print(a[1]) -- propagates error v, err = a:join() -- no propagation if v == nil then error("'a' faced error"..tostring(err)) -- manual propagation end |
If you want to wait for multiple lanes to finish (any of a set of lanes), use a linda object. Give each lane a specific id, and send that id over a linda once that thread is done (as the last thing you do).
local lanes = require "lanes" local sync_linda = lanes.linda() f = lanes.gen(function() dostuff() sync_linda:send("done", true) end) a = f() b = f() c = f() sync_linda:receive(nil, sync_linda.batched, "done", 3) -- wait for 3 lanes to write something in "done" slot of sync_linda |
bool[,reason] = lane_h:cancel("soft" [, timeout] [, wake_lane]) bool[,reason] = lane_h:cancel("hard" [, timeout] [, wake_lane]) bool[,reason] = lane_h:cancel([mode, hookcount] [, timeout] [, wake_lane]) |
timeout is an optional number >= 0. Defaults to 0 if left unspecified or nil.
cancel() sends a cancellation request to the lane.
First argument is a mode can be one of:
Returns true, lane_h.status if lane was already done (in "done", "error" or "cancelled" status), or the cancellation was fruitful within timeout_secs timeout period.
Returns false, "timeout" otherwise.
If the lane is still running after the timeout expired, there is a chance lanes will freeze forever at shutdown when failing to terminate all free-running lanes within the specified timeout.
Cancellation is tested before going to sleep in receive() or send() calls and after executing cancelstep Lua statements. A pending receive()or send() call is awakened.
This means the execution of the lane will resume although the operation has not completed, to give the lane a chance to detect cancellation (even in the case the code waits on a linda with infinite timeout).
The code should be able to handle this situation appropriately if required (in other words, it should gracefully handle the fact that it didn't receive the expected values).
It is also possible to manually test for cancel requests with cancel_test().
set_finalizer(finalizer_func) void = finalizer_func([err, stack_tbl]) |
The regular Lua error function is usable in lanes for throwing exceptions. What Lua does not offer, however, is scoped finalizers that would get called when a certain block of instructions gets exited, whether through peaceful return or abrupt error.
Lanes registers a function set_finalizer() in the lane's Lua state for doing this. Any functions given to it will be called in the lane Lua state, just prior to closing it. It is possible to set more than one finalizer. They are not called in any particular order.
An error in a finalizer itself overrides the state of the regular chunk (in practice, it would be highly preferable not to have errors in finalizers). If one finalizer errors, the others may not get called. If a finalizer error occurs after an error in the lane body, then this new error replaces the previous one (including the full stack trace).
local lane_body = function() set_finalizer(function(err, stk) if err and type(err) ~= "userdata" then -- no special error: true error print(" error: "..tostring(err)) elseif type(err) == "userdata" then -- lane cancellation is performed by throwing a special userdata as error print("after cancel") else -- no error: we just got finalized print("finalized") end end) end |
Communications between lanes is completely detached from the lane handles themselves. By itself, a lane can only provide return values once it is finished, or throw an error. Needs to communicate during runtime are handled by linda objects, which are deep userdata instances. They can be provided to a lane as startup arguments, upvalues or in some other linda's message.
Access to a linda object means a lane can read or write to any of its data slots. Multiple lanes can be accessing the same linda simultaneously. Seen from Lua, no application level locking is required; each linda operation is atomic.
local lanes = require "lanes" local linda = lanes.linda("my linda") local function loop(max) for i = 1, max do print("sending: " .. i) linda:send("x", i) -- linda as upvalue of loop() end end lane_h = lanes.gen("", loop)(10000) while true do local slot, val = linda:receive(3.0, "x") -- timeout in seconds if val == nil then print("timed out") break end print(tostring(linda) .. " received: " .. val) end lane_h:join() |
Characteristics of the Lanes implementation of lindas are:
h = lanes.linda([name],[group],[close_handler]) |
Arguments to lanes.linda() can be provided in any order, as long as there is a single string, a single number, and a single callable value (all are optional).
Converting the linda to a string will yield the provided name prefixed by "Linda: ".
If opt_name is omitted, it will evaluate to an hexadecimal number uniquely representing that linda when the linda is converted to a string. The value is the same as returned by linda:deep().
If opt_name is "auto", Lanes will try to construct a name from the source location that called lanes.linda(). If that fails, the linda name will be "<unresolved>".
If Lanes is configured with more than one Keeper state, group is mandatory.
If the linda is to-be-closed (Lua 5.4+), and a close_handler is provided, it will be called with all the provided arguments. For older Lua versions, its presence will cause an error.
bool,string|(nil,[lanes.cancel_error|"timeout"]) = h:limit(slot, <limit>) (number|string),string = h:limit(slot) |
By default, queue sizes are unlimited but limits can be enforced using the limit() method. This can be useful to balance execution speeds in a producer/consumer scenario.
A limit of 0 is allowed to block everything. "unlimited" removes the limit.
If the slot was full but the limit change added some room, limit() first return value is true and the linda is signalled so that send()-blocked threads are awakened, else the return value is false.
If no limit is provided, limit() first return value is the current limit for the specified slot.
The second returned value is a string representing the fill status relatively to the slot's current limit (one of "over", "under", "exact").
Whether reading or writing, if the linda is cancelled, limit() returns nil, lanes.cancel_error.
string|(nil,[lanes.cancel_error|"timeout"]) = h:restrict(slot [, "<mode>"]) string = h:restrict(slot) |
It is possible to restrict a particular slot in a Linda to either send/receive or set/get operations.
Possible modes are "none", "set/get" or "send/receive".
If a new mode is specified, restrict() updates the mode and returns the previous one.
If no mode is specified, restrict() does nothing and returns the current mode.
If the linda is cancelled, restrict() returns nil, lanes.cancel_error.
If an unknown mode is specified, restrict() raises an error.
true|lanes.cancel_error = h:send([timeout_secs,] slot, ...) |
Timeouts are given in seconds (>= 0, millisecond accuracy) or nil. Timeout can be omitted only if the first slot is not a number (then it is equivalent to an infinite duration).
Each slot acts as a FIFO queue. There is no limit to the number of slots a linda may contain. Different lindas can have identical slots, which are totally unrelated.
Multiple values can be sent to a given slot at once, atomically (the send will fail unless all the values fit within the queue limit). This can be useful for multiple producer scenarios, if the protocols used are giving data in streams of multiple units. Atomicity avoids the producers from garbling each others messages, which could happen if the units were sent individually.
If linda.null or lanes.null is sent as data in a linda, it will be read as a nil.
send() raises an error if no data is provided after the slot.
send() raises an error if called when a restriction forbids its use on the provided slot.
send() raises lanes.cancel_error if interrupted by a hard cancel request.
send() return values can be:
slot, val = h:receive([timeout_secs,] slot [, slot...]) slot, val [, val...] = h:receive([timeout,] h.batched, slot, n_uint_min[, n_uint_max]) |
receive() raises an error if called when a restriction forbids its use on any provided slot.
In batched mode, receive() will raise an error if min_count < 1 or max_count < min_count.
Unbatched receive() return values can be:
Note that any number of lanes can be reading or writing a linda. There can be many producers, and many consumers. It is up to you.
Remember that Hard cancellation will cause pending linda operations to abort execution of the lane through a cancellation error. This means that you have to install a finalizer in your lane if you want to run some code in that situation.
When receiving from multiple slots, the slots are checked in order, which can be used for making priority queues.
(bool,string)|(nil,lanes.cancel_error) = linda_h:set(slot [, val [, ...]]) (number,[val [, ...]])|(nil,lanes.cancel_error) = linda_h:get(slot [, count = 1]) |
set() and get() raise an error if used when a restriction forbids their use on the provided slot.
Unless a particular slot is constrained with restrict(), get()/set() and send()/receive() can be used together; reading a slot essentially peeks the next outcoming value of a queue.
get()/set() are for accessing a slot without queuing or consuming. They can be used for making shared tables of storage among the lanes.
set() never blocks because it ignores the limit. It overwrites existing values and clears any possible queued entries.
get() can read several values at once, and does not block. Return values ares:
set() can write several values at the specified slot. Writing nil values is possible, and clearing the contents at the specified slot is done by not providing any value.
If set() actually stores data, the linda is signalled for write, so that receive()-blocked Lanes are awakened.
Clearing the contents of a non-existent slot does not create it!
If the slot was full but the new data count of the slot after set() is below its limit, set() first return value is true and the linda is also signaled for read, so that send()-blocked Lanes are awakened.
If the slot was not already full, nothing additional happens, and set() first return value is false.
The second return value is a string representing the fill status relatively to the slot's current limit (one of "over", "under", "exact").
Trying to send or receive data through a cancelled linda does nothing and returns lanes.cancel_error.
linda_h:collectgarbage() |
Forces a full garbage collect cycle inside the Keeper state assigned to the Linda (same as lua_gc(L, LUA_GCCOLLECT)).
Can be useful to clean stale storage after some keys are cleaned.
[val] = linda_h:count([slot[,...]]) |
Returns some information about the contents of the linda.
If no slot is specified, and the linda is empty, returns nothing.
If no slot is specified, and the linda is not empty, returns a table of slot/count pairs that counts the number of items in each of the exiting slots of the linda. This count can be 0 if the slot has been used but is empty.
If a single slot is specified, returns the number of pending items, or nothing if the slot is unknown.
If more than one slot is specified, return a table of slot/count pairs for the known slots.
linda_h:dump() -> { [<slot>] = { first = <n> count = <n> limit = <n>|'unlimited' restrict = "none"|"set/get"|"send/receive" fifo = { <array of values> } } ... } |
Returns a table describing the full contents of a linda, or nil if the linda wasn't used yet.
If Decoda support is enabled with HAVE_DECODA_SUPPORT(), the linda metatable contains a __towatch special function that generates a similar table used for debug display.
void = linda_h:cancel("read"|"write"|"both"|"none") "cancelled"|"active" = linda_h.status |
linda:cancel() signals the linda so that lanes waiting for read, write, or both, wake up.
All linda operations (including get() and set()) will return lanes.cancel_error as when the calling lane is soft-cancelled as long as the linda is marked as cancelled.
"none" reset the linda's cancel status, but doesn't signal it.
linda.status reads the current cancel status.
If not void, the lane's cancel status overrides the linda's cancel status.
void = linda_h:wake("read"|"write"|"both") |
Signals the linda so that lanes waiting for read, write, or both, wake up. Does not change the current Linda's cancel status. Can be used to break early out of a blocking wait.
light userdata = h:deep() |
Returns a light userdata that uniquely represents the linda. The stored value is the same as what is seen when converting an unnamed linda to a string.
A linda is a gateway to read and write data inside some hidden Lua states, called keeper states. Lindas are hashed to a fixed number of keeper states, which are a locking entity.
The data sent through a linda is stored inside the associated keeper state in a Lua table where each linda slot is the key to another table containing a FIFO for that slot.
Each keeper state is associated with an OS mutex, to prevent concurrent access to the keeper state. The linda itself uses two signals to be made aware of operations occuring on it.
Whenever Lua code reads from or writes to a linda, the mutex is acquired. If linda limits don't block the operation, it is fulfilled, then the mutex is released.
If the linda has to block, the mutex is released and the OS thread sleeps, waiting for a linda operation to be signalled. When an operation occurs on the same linda, possibly fufilling the condition, or a timeout expires, the thread wakes up.
If the thread is woken but the condition is not yet fulfilled, it goes back to sleep, until the timeout expires.
When a lane is cancelled, the signal it is waiting on (if any) is signalled. In that case, the linda operation will return lanes.cancel_error.
A single linda object provides an infinite number of slots, so why would you want to use several?
There are some important reasons:
Actually, you can. Make separate lanes to wait each, and then multiplex those events to a common linda, but... :).
void = lanes.timer(linda_h, slot, date_tbl|first_secs [,period_secs]) |
Timers are implemented as a lane. They can be enabled by setting "with_timers" to true in lanes.configure() settings.
Timers can be run once, or in a reoccurring fashion (period_secs > 0). The first occurrence can be given either as a date or as a relative delay in seconds. The date table is like what os.date("*t") returns, in the local time zone.
Once a timer expires, the slot is set with the current time (in seconds, same offset as os.time() but with millisecond accuracy). The slot can be waited upon using the regular linda :receive() method.
A timer can be stopped simply with first_secs=0|nil and no period.
local lanes = require "lanes" lanes.configure{with_timers = true} local linda = lanes.linda() -- First timer once a second, not synchronized to wall clock -- lanes.timer(linda, "sec", 1, 1) -- Timer to a future event (next even minute); wall clock synchronized -- local t = os.date("*t", os.time() + 60) -- now + 1min t.sec = 0 lanes.timer(linda, "min", t, 60) -- reoccur every minute (sharp) while true do local slot, v = linda:receive("sec", "min") print("Timer "..slot..": "..v) end |
NOTE: Timer slots are set, not queued, so missing a beat is possible especially if the timer cycle is extremely small. The slot value can be used to know the actual time passed.
Having the API as lanes.timer() is intentional. Another alternative would be linda_h:timer() but timers are not traditionally seen to be part of lindas. Also, it would mean any lane getting a linda handle would be able to modify timers on it. A third choice could be abstracting the timers out of linda realm altogether (timer_h= lanes.timer(date|first_secs, period_secs )) but that would mean separate waiting functions for timers, and lindas. Even if a linda object and slot was returned, that slot couldn't be waited upon simultaneously with one's general linda events. The current system gives maximum capabilities with minimum API, and any smoothenings can easily be crafted in Lua at the application level. |
{[{linda, slot, when, period}[,...]]} = lanes.timers() |
The full list of active timers can be obtained. Obviously, this is a snapshot, and non-repeating timers might no longer exist by the time the results are inspected.
Can return nil, "timeout" or nil, lanes.cancel_error in case of interruption.
nil, "timeout" = lanes.sleep(['indefinitely'|seconds|nil]) |
A very simple way of sleeping when nothing else is available. Is implemented by attempting to read some data in an unused channel of the internal linda used for timers (this linda exists even when timers aren't enabled).
Default duration is 0, which should only cause a thread context switch.
Return values should always be nil, "timeout" (or nil, lanes.cancel_error in case of interruption).
number = lanes.now_secs() |
Returns the current value of the clock used by timers and linda objects.
Lanes does not generally require locks or critical sections to be used, at all. If necessary, a limited queue can be used to emulate them. lanes.lua offers some sugar to make it easy:
lock_func|lanes.cancel_error = lanes.genlock(linda_h, slot [,N_uint=1]) bool|lanes.cancel_error = lock_func(M_uint [, "try"] ) -- acquire .. bool|lanes.cancel_error = lock_func(-M_uint) -- release |
The generated function acquires M tokens from the N available, or releases them if the value is negative. The acquiring call will suspend the lane, if necessary. Use M=N=1 for a critical section lock (only one lane allowed to enter).
When passsing "try" as second argument when acquiring, then lock_func operates on the linda with a timeout of 0 to emulate a TryLock() operation. If locking fails, lock_func returns false. "try" is ignored when releasing (as it it not expected to ever have to wait unless the acquisition/release pairs are not properly matched).
Upon successful lock/unlock, lock_func returns true (always the case when block-waiting for completion).
Note: The generated locks are not recursive (A single lane locking several times will consume tokens at each call, and can therefore deadlock itself). That would need another kind of generator, which is currently not implemented.
Similar sugar exists for atomic counters:
atomic_func|lanes.cancel_error = lanes.genatomic(linda_h, slot [,initial_num=0.0]) new_num|lanes.cancel_error = atomic_func([diff_num=+1.0]) |
Each time called, the generated function will change linda[slot] atomically, without other lanes being able to interfere. The new value is returned. You can use either diff 0.0 or get to just read the current value.
Note that the generated functions can be passed on to other lanes.
Data passed between lanes (either as starting arguments, return values, upvalues or via lindas) must conform to the following:
C functions and full userdata are transfered as follows (more or less):
// expects a C function or full userdata on top of the source Lua stack copyValue(lua_State *dest, lua_State* source) { // fetch function 'name' from source lookup database char const* valuename = lookup_name(source, -1); // lookup a function or userdata bound to this name in the destination state, and push it on the stack push_resolved_value(dest, valuename); } |
The devil lies in the details: what does "lookup" mean?
Since functions and full userdata are first class values, they don't have a name. All we know for sure is that when a C module registers some functions or full userdata, they are accessible to the script that required the module through some exposed variables.
For example, loading the string base library creates a table accessible when indexing the global environment with key "string". Indexing this table with "match", "gsub", etc. will give us a function.
Similarly, loading the io base library creates a table accessible when indexing the global environment with key "io". Indexing this table with "open", will give us a function, and "stdin" will give us a full userdata.
When a lane generator creates a lane and performs initializations described by the list of base libraries and the list of required modules, it recursively scans the table created by the initialisation of the module, looking for all values that are C functions and full userdata.
Each time a function or full userdata is encountered, the sequence of keys traversed to reach it is contatenated in a (hopefully) unique name. The [name, value] and [value, name] pairs are both stored in a lookup table in all involved Lua states (main Lua state and lanes states).
Then, when a function or full userdata is transfered from one state to another, all we have to do is retrieve the name associated to this value in the source Lua state, then with that name retrieve the equivalent value that already exists in the destination state.
Note that there is no need to transfer upvalues/uservalues, as they are already bound to the value registered in the destination state. (And in any event, it is not possible to create a closure from a C function pushed on the stack, it can only be created with a lua_CFunction pointer).
There are several issues here:
string2 = string |
Most Lua extension modules should work unaltered with Lanes. If the module simply ties C side features to Lua, everything is fine without alterations. The luaopen_...() entry point will be called separately for each lane, where the module is require'd from.
If it, however, also does one-time C side initializations, these should be covered into a one-time-only construct such as below.
int luaopen_module(lua_State *L ) { static char been_here; /* 0 by ANSI C */ // Calls to 'require' serialized by Lanes; this is safe. if (!been_here) { been_here= 1; ... one time initializations ... } ... binding to Lua ... } |
An alternative way of passing full userdata across lanes uses a new __lanesclone metamethod.
When a deep userdata is cloned, Lanes calls __lanesclone once, in the context of the source lane.
The call receives the clone and original as light userdata, plus the actual userdata size, as in clone:__lanesclone(original,size), and should perform the actual cloning.
A typical implementation would look like:
static int clonable_lanesclone(lua_State* L) { switch(lua_gettop(L)) { case 3: { struct s_MyClonableUserdata* self = lua_touserdata(L, 1); struct s_MyClonableUserdata* from = lua_touserdata(L, 2); size_t len = lua_tointeger(L, 3); assert(len == sizeof(struct s_MyClonableUserdata)); *self = *from; } return 0; default: std::ignore = luaL_error(L, "Lanes called clonable_lanesclone with unexpected arguments"); } return 0; } |
NOTE: In the event the source userdata has uservalues, it is not necessary to create them for the clone, Lanes will handle their cloning.
Of course, more complex objects may require smarter cloning behavior than a simple memcpy. Also, the module initialisation code should make each metatable accessible from the module table itself as in:
int luaopen_deep_userdata_example(lua_State* L) { luaL_newlib(L, deep_module); // preregister the metatables for the types we can instanciate so that Lanes can know about them if (luaL_newmetatable(L, "clonable")) { luaL_setfuncs(L, clonable_mt, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); } lua_setfield(L, -2, "__clonableMT"); // actual name is not important if (luaL_newmetatable(L, "deep")) { luaL_setfuncs(L, deep_mt, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); } lua_setfield(L, -2, "__deepMT"); // actual name is not important return 1; } |
Then a new clonable userdata instance can just do like any non-Lanes aware userdata, as long as its metatable contains the aforementionned __lanesclone method.
int luaD_new_clonable(lua_State* L) { lua_newuserdata(L, sizeof(struct s_MyClonableUserdata)); luaL_setmetatable(L, "clonable"); return 1; } |
The mechanism Lanes uses for sharing linda handles between separate Lua states can be used for custom userdata as well. Here's what to do.
class MyDeepFactory : public DeepFactory { private: void createMetatable(lua_State* const L_) const override; void deleteDeepObjectInternal(lua_State* const L_, DeepPrelude* o_) const override; DeepPrelude* newDeepObjectInternal(lua_State* const L_) const override; std::string_view moduleName() const override; }; static MyDeepFactory g_MyDeepFactory; |
Deep userdata management will take care of tying to __gc methods, and doing reference counting to see how many proxies are still there for accessing the data. Once there are none, the data will be freed through a call to the factory you provided.
Pay attention to the fact a deep userdata last proxy can be held inside a Keeper state after being stored in a Linda and all other references are lost. If the Linda then flushes its contents through garbage collection in the Keeper state or by being collected itself, it means that deleteDeepObjectInternal() can be called from inside a Keeper state.
NOTE: The lifespan of deep userdata may exceed that of the Lua state that created it. The allocation of the data storage should not be tied to the Lua state used. In other words, use new/delete, malloc()/free() or similar memory handling mechanism.
Lane handles are not implemented as deep userdata, and cannot thus be copied across lanes. This is intentional; problems would occur at least when multiple lanes were to wait upon one to get ready. Also, it is a matter of design simplicity.
The same benefits can be achieved by having a single worker lane spawn all the sublanes, and keep track of them. Communications to and from this lane can be handled via a linda.
In multithreaded scenarios, giving multiple arguments to print() or file:write() may cause them to be overlapped in the output, something like this:
A: print(1, 2, 3, 4 ) B: print('a', 'b', 'c', 'd' ) 1 a b 2 3 c d 4 |
Lanes is about making multithreading easy, and natural in the Lua state of mind. Expect performance not to be an issue, if your program is logically built. Here are some things one should consider, if best performance is vital:
Cancellation of lanes uses the Lua error mechanism with a special lightuserdata error sentinel. If you use pcall in code that needs to be cancellable from the outside, the special error might not get through to Lanes, thus preventing the lane from being cleanly cancelled. You should throw any lightuserdata error further.
This system can actually be used by application to detect cancel, do your own cancellation duties, and pass on the error so Lanes will get it. If it does not get a clean cancellation from a lane in due time, it may forcefully kill the lane.
The sentinel is exposed as lanes.cancel_error, if you wish to use its actual value.
See CHANGES.
For feedback, questions and suggestions: