Multithreaded Lua by Effil

2017-09-28

Hello, everyone!

I’m glad to present you the first release of project - Effil v1.0-0. Developed by me and Udalov Ilya.

Effil is a lua library for multithreading support. It allows to spawn native OS threads and safely exchange data between them. Effil has been designed to provide clear and simple API for lua developers and in general it includes threads, channels and shared tables.

The sources and documentation are available on GitHub.

Threads

General purpose of Effil is multithreading. So, you can run threads like that:

effil = require "effil"

-- Here is some function which will work in another thread
function foo(a, b)
    return a + b
end

-- Create a thread runner and run a thread with specific parameters
thr = effil.thread(foo)(1, 2)

-- Wait untill thread completion and obtain result
result = thr:get()

It’s extremely simple!

Effil’s threads are manageable. It means that you can pause, resume and cancel them. You can check status of thread using status method, wait for thread completion using wait method with some timeout or infinitely.

function foo()
    while true do end
end

thr = effil.thread(foo)()

thr:status() -- returns "running"

thr:pause()
thr:status() -- returns "paused"

thr:resume()
thr:status() -- returns "running"

thr:cancel()
thr:status() -- returns "canceled"

Each thread uses a separate Lua state and to communicate between them Effil provides channels and shared tables.

Channels

Effil’s channel implements First-In-First-Out queue and can be used for data exchange and threads synchronizations. Channel is pretty simple:

  • You can push to and pop from channel (remember it’s FIFO)
  • You can limit the capacity of channel or leave it unlimited
  • You can pop message from queue asynchronously or wait for message appearance with timeout or infinitely
channel = effil.channel()

function worker(channel)
    msg = channel:pop() -- wait for message
    channel:push("Got message: " .. msg)
end

thread = effil.thread(worker)(channel)

channel:push("Hello there!")
print(channel:pop())  -- wait for message and print it

thread:wait() -- just wait for thread completion

Shared tables

Shared table is our implementation of Lua table which can be used in different threads. It works like a regular Lua table as similar as possible:

-- Create an empty shared table or initialize it with regular table
t = effil.table()
-- Put any data you want
t.key = "value"
t[1] = function() end
t[2] = true
t[3] = 1.2
t.embedded = { 1, 2, 3, 4}

-- Iterate over it
for k, v in pairs(t) do
    -- ...
end

And of course you can transmit it between threads, it’s safe!

t = effil.table()

function worker(t)
    t.key = "value"
end

thread = effil.thread(worker)(t)
thread:wait()

print(t.key) -- will print "value"

Metatables

Our shared tables have metatable just like Lua’s.

mt = effil.table {
    __add = function(l, r)
        return l.value + r.value
    end
}

t1 = effil.table { value = 10 }
t2 = effil.table { value = 20 }

-- Just set a metatable to table like in pure Lua
effil.setmetatable(t1, mt)
effil.setmetatable(t2, mt)

print(t1 + t2)

We support a wide list of standard Lua metamethods. Here is the current set:

__pairs
__ipairs
__newindex
__index
__len
__tostring
__add
__sub
__mul
__div
__mod
__pow
__concat
__lt
__unm
__call
__eq
__le

What will be if I pass a regular Lua table to effil?

Don’t worry, Effil always converts any Lua table to Effil’s shared table. It means that the one who should receive table passed through Effil will always get a shared table. Let’s take a look at the example:

t = { 1, 8, 33 }

thread = effil.thread(
    function(t)
        local sum = 0
        for _, v in ipairs(t) do
            sum = sum + v
        end
        return { sum = sum, amount = #t }
    end
)(t)

ret = thread:get()

print(effil.type(ret)) -- -- prints 'effil.table'

print(ret.sum) -- prints '42'
print(ret.amount) -- prints '3'

Who cares about shared table’s lifetime?

Our shared tables are automatically collected as any Lua object. And for this purpose we have written our own garbage collector. We use Tracing Garbage Collector similar to Lua GaC. We delete Effil’s object if there is no live reference from Lua to it or from another live Effil’s object.

Our GC has a simple and clear API. You can pause it effil.gc.pause() and resume effil.gc.resume(), trigger garbage collecting effil.gc.collect() and check amount of allocated objects effil.gc.count(). And of course you can configure it using effil.gc.step() method. The current implementation of GC is pretty naive, but will be improved in the next release.

What about limitations?

Of course we have some limits. Effil does not work with coroutines and custom userdata. To support custom userdata you should extend Effil library and rebuild it.

The library does not support upvalues, but it’s planned to be implemented in the next release.

Effil is cross platform. You can run it on any operating system if it has C++ compiler which supports C++14. E.g. it works with gcc-4.9, Visual Studio 2015 and clang-3.8.


We welcome your feedback and suggestions.

Join our project on github!