effil/README.md
2017-07-21 23:00:10 +03:00

12 KiB

Effil

Build Status Build Status Windows Documentation Status

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

  1. git clone git@github.com:loud-hound/effil.git effil && cd effil
  2. mkdir build && cd build && make -j4 install
  3. 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:

  1. effil.thread(action) - function which creates threads.
  2. effil.table - table that persist in all threads and behaves just like regular lua table.
  3. 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.thread
  • effil.table
  • effil.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.path value for new state. Default value inherits package.path form parent state.
  • runner.path - package.cpath value for new state. Default value inherits package.cpath form 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.table and effil.channel share 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 or nil in 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 from tbl.
  • effil.size(tbl) - get number of entries in table.
  • effil.setmetatable(tbl, mtbl) - set metatable. mtbl can be regular or shared table.
  • effil.getmetatable(tbl) - returns current metatable. Returned table always has type effil.table. Default metatable is nil.
  • 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. If capacity equals to 0 size of channel is unlimited. Default capacity is 0.
  • chan:push() - push value. Returns true if value fits channel capacity, false otherwise. 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 equals 0 then 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.G is always present.
  • effil.gc.step() - get GC step. Default is 200.
  • 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:

  1. invoke collectgarbage() in all threads.
  2. invoke effil.gc.collect() in any thread.