11 KiB
Effil
Effil is a lua module for multithreading support. It allows to spawn native threads and safe data exchange. Effil has been designed to provide clear and simple API for lua developers including threads, channels and shared tables.
Effil supports lua 5.1, 5.2, 5.3 and LuaJIT. Requires C++14 compiler compliance. Tested with GCC 5, clang 3.8 and Visual Studio 2015.
How to install
Build from src on Linux and Mac
git clone git@github.com:loud-hound/effil.git effil && cd effilmkdir build && cd build && make -j4 install- Copy effil.lua and libeffil.so/libeffil.dylib to your project.
From lua rocks
Coming soon.
Quick guide for impatient
As you may now 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 in separate native thread and provides robust communicating primitives for creating threads (VM instances) and data sharing.
Effil library provides two major functions:
effil.thread(action)- function which creates threads.effil.table- table that persist in all threads and behaves just like regular lua table.- Bunch og utilities to handle threads and tables.
Examples
Spawn the thread
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()
Output:
Sparky barks from another thread!
Shareing data with effil.channel
local effil = require("effil")
-- channel allow to push date 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()
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
local effil = require("effil")
-- effil.table transfers data between threads
-- and behaves like regualr lua table
local storage = effil.table {}
function download_file(storage, url)
local restult = {}
restult.downloaded = true
restult.content = "I am form " .. url
restult.bytes = #restult.content
storage[url] = restult
end
-- capture download function
local downloader = effil.thread(download_file)
local downloads = effil.table {}
for _, url in pairs({"luarocks.org", "ya.ru", "github.com" }) do
-- run downloads in separate threads
-- each invocation creates separate thread
downloads[#downloads + 1] = downloader(storage, url)
end
for _, download in pairs(downloads) do
download:wait()
end
for url, result in pairs(storage) do
print('From ' .. url .. ' downloaded ' ..
result.bytes .. ' bytes, content: "' .. result.content .. '"')
end
Output:
From github.com downloaded 20 bytes, content: "I am form github.com"
From luarocks.org downloaded 22 bytes, content: "I am form luarocks.org"
From ya.ru downloaded 15 bytes, content: "I am form ya.ru"
API Reference
Effil provides:
effil.threadeffil.tableeffil.channel- set of helper functions.
Implementation
All effil features implemented in C++ with great help of sol2. It requires C++14 compliance (GCC 4.9, Visual Studio 2015 and clang 3.?).
effil.thread
Overview
effil.thread is a way to create thread. Threads can be stopped, paused, resumed and canceled.
All operation with threads can be synchronous (with or without timeout) or asynchronous.
Each thread runs with its own lua state.
Do not run function with upvalues in effil.thread
Use effil.table and effil.channel to transmit data over threads.
Example
local function greeting(name)
return "Hello, " .. name
end
local effil = require "effil"
local runner = effil.thread(greeting) -- capture function to thread runner
local thr1 = runner("Sparky") -- create first thread
local thr2 = runner() -- create second thread
print(thr1:get()) -- get result
print(thr2:get()) -- get result
API
runner = effil.threas(f) - creates thread runner. Runner spawn new thread for each invocation.
Thread runner
runner.path-package.pathvalue for new state. Default value inheritspackage.pathform parent state.runner.path-package.cpathvalue for new state. Default value inheritspackage.cpathform parent state.runner.step- number of lua instructions lua between cancelation points. Default value is 200. Spawn unstopable threads when value is equal to 0.thr = runner(arg1, arg2, arg3)- run captured function with this args in separate thread and returns handle.
Thread handle
Thread handle provides API for interation with shild thread.
- You can use
effil.tableandeffil.channelshare this handles between threads. - You can call any handle methods from multiple threads.
- You don't need to save this handle if you do not want to communicate with thread.
All functions:
thr:status()- return thread status. Possible values are:"running", "paused", "canceled", "completed" and "failed"thr:get(time, metric)- waits for thread completion and returns function result ornilin case of error.thr:wait(time, metric)- waits for thread completion and returns thread status with error message is"failed".thr:cancel(time, metric)- interrupt thread execution.thr:pause(time, metric)- pause thread.thr:resume(time, metric)- resume thred.
All operations can be bloking or non blocking.
thr:get()- blocking wait for thread completion.thr:get(0)- non blocking get.thr:get(50, "ms")- blocking wait for 50 milliseconds.the:get(1)- blocking wait for 1 second.
Metrics:
ms- milliseconds;s- seconds;m- minutes;h- hours.
Thread helpers
effil.thread_id() - unique string thread id.
effil.yield() - explicit cancellation point.
effil.sleep(time, metric) - suspend current thread. metric is optional and default is seconds.
effil.table
Overview
effil.table is a way to exchange data between effil threads.
It behaves almost like standard lua tables.
All operations with shared table are thread safe.
Shared table stores primitive types (number, boolean, string),
function, table, light userdata and effil based userdata.
Shared table doesn't store lua threads (coroutines) or arbitrary userdata.
Example
local effil = require "effil"
local pets = effil.table()
pets.sparky = effil.table { says = "wof" }
assert(pets.sparky.says == "wof")
API
effil.table()- create new empty shared table.effil.table(tbl)- create new shared table and fill it with values fromtbl.effil.size(tbl)- get number of entries in table.effil.setmetatable(tbl, mtbl)- set metatable.mtblcan be regular or shared table.effil.getmetatable(tbl)- returns current metatable. Returned table always has typeeffil.table. Default metatable isnil.effil.rawset(tbl, key, value)- set table entry without invoking metamethod__newindex.effil.rawget(tbl, key)- get table value without invoking metamethod__index.
Shared tables with regular tables
If you want to store regular table in shared table, effil will implicitly dump origin table into new shared table. Shared tables always stores subtables as shared tables.
Shared tables with functions
If you want to store function in shared table, effil will implicitly dump this function and saves it in internal representation as string. Thus, all upvalues will be lost. Do not store function with upvalues in shared tables.
Global shred table
effil.G is a global predefined shared table.
This table always present in any thread.
Type identification
Use effil.type to deffer effil.table for other userdata.
assert(effil.type(effil.table()) == "effil.table")
effil.channel
Overview
effil.channel is a way to sequentially exchange data between effil threads.
It allows push values from one thread and pop them from another.
All operations with channels are thread safe.
Channel passes primitive types (number, boolean, string),
function, table, light userdata and effil based userdata.
Channel doesn't pass lua threads (coroutines) or arbitrary userdata.
Example
local effil = require "effil"
local chan = effil.channel()
chan:push(1, "Wow")
chan:push(2, "Bark")
local n, s = chan:pop()
assert(1 == n)
assert("Wow" == s)
assert(chan:size() == 1)
API
chan = effil.channel(capacity)- channel capacity. Ifcapacityequals to0size of channel is unlimited. Default capacity is0.chan:push()- push value. Returnstrueif value fits channel capacity,falseotherwise. Supports multiple values.chan:pop()- pop value. If value is not present, wait for the value.chan:pop(time, metric)- pop value with timeout. If time equals0then pop asynchronously.chan:size()- get actual size of channel.
Metrics
ms- milliseconds;s- seconds;m- minutes;h- hours.
effil.type
Threads, channels and tables are userdata.
Thus, type() will return userdata for any type.
If you want to detect type more precisely use effil.type.
It behaves like regular type(), but it can detect effil specific userdata.
There is a list of extra types:
effil.type(effil.thread()) == "effil.thread"effil.type(effil.table()) == "effil.table"effil.type(effil.channel() == "effil.channel"
effil.gc
Overview
Effil provides custom garbage collector for effil.table and effil.table.
It allows safe manage cyclic references for tables and channels in multiple threads.
However it may cause extra memory usage.
effil.gc provides a set of method configure effil garbage collector.
But, usually you don't need to configure it.
Garbage collection trigger
Garbage collection may occur with new effil object creation (table or channel). Frequency of triggering configured by GC step. For example, if Gc step is 200, then each 200'th object creation trigger GC.
API
effil.gc.collect()- force garbage collection, however it doesn't guarantee deletion of all effil objects.effil.gc.count()- show number of allocated shared tables and channels. Minimum value is 1,effil.Gis always present.effil.gc.step()- get GC step. Default is200.effi.gc.step(value)- set GC step and get previous value.effil.gc.pause()- pause GC.effil.gc.resume()- resume GC.effil.gc.enabled()- get GC state.
How to cleanup all dereferenced objects
Each thread represented as separate state with own garbage collector.
Thus, objects will be deleted eventually.
Effil objects itself also managed by GC and uses __gc userdata metamethod as deserializer hook.
To force objects deletion:
- invoke
collectgarbage()in all threads. - invoke
effil.gc.collect()in any thread.