Linux + MacOS | 窓 |
---|---|
Effil は Lua 用のマルチスレッド ライブラリです。これにより、ネイティブ スレッドの生成と安全なデータ交換が可能になります。 Effil は、lua 開発者に明確でシンプルな API を提供するように設計されています。
Effil は lua 5.1、5.2、5.3 および LuaJIT をサポートします。 C++14 コンパイラへの準拠が必要です。 GCC 4.9+、clang 3.8、および Visual Studio 2015 でテスト済み。
git clone --recursive https://github.com/effil/effil effil
cd effil && mkdir build && cd build
cmake .. && make install
luarocks install effil
ご存知かもしれませんが、実際のマルチスレッドをサポートするスクリプト言語はそれほど多くありません (Lua/Python/Ruby などにはグローバル インタプリタ ロック、別名 GIL があります)。 Effil は、独立した Lua VM インスタンスを個別のネイティブ スレッドで実行することでこの問題を解決し、スレッドの作成とデータ共有のための堅牢な通信プリミティブを提供します。
Effil ライブラリは 3 つの主要な抽象化を提供します。
effil.thread
- スレッド管理用の API を提供します。effil.table
- テーブル管理用の API を提供します。テーブルはスレッド間で共有できます。effil.channel
- 順次データ交換のための先入れ先出しコンテナを提供します。スレッドやテーブルを処理するためのユーティリティも多数あります。
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 ()
出力: Sparky barks from another thread!
local effil = require ( " effil " )
-- channel allow to push data 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 ()
出力:
push 1
push 2
pop 1
pop 2
push 3
push 4
push 5
pop 3
pop 4
pop 5
effil = require ( " effil " )
-- effil.table transfers data between threads
-- and behaves like regualr lua table
local storage = effil . table { string_field = " first value " }
storage . numeric_field = 100500
storage . function_field = function ( a , b ) return a + b end
storage . table_field = { fist = 1 , second = 2 }
function check_shared_table ( storage )
print ( storage . string_field )
print ( storage . numeric_field )
print ( storage . table_field . first )
print ( storage . table_field . second )
return storage . function_field ( 1 , 2 )
end
local thr = effil . thread ( check_shared_table )( storage )
local ret = thr : get ()
print ( " Thread result: " .. ret )
出力:
first value
100500
1
2
Thread result: 3
Effil では、 effil.channel
、 effil.table
使用するか、 effil.thread
のパラメータとして直接、スレッド間 (Lua インタプリタ状態) でデータを送信できます。
nil
、 boolean
、 number
、 string
lua_dump
を使用してダンプされます。上位値はルールに従ってキャプチャされます。lua_iscfunction
が true を返すもの) は、 lua_tocfunction
(元の lua_State 内) および lua_pushcfunction (新しい lua_State 内) を使用してポインターだけで送信されます。lua_iscfunction
true を返しますが、 lua_tocfunction
nullptr を返します。そのため、lua_State 間でそれを送信する方法が見つかりません。effil.table
にシリアル化されます。したがって、Lua テーブルはすべてeffil.table
になります。テーブルが大きい場合、テーブルのシリアル化に時間がかかることがあります。したがって、テーブルのシリアル化を避けて、データをeffil.table
に直接配置することをお勧めします。 2 つの例を考えてみましょう。 -- Example #1
t = {}
for i = 1 , 100 do
t [ i ] = i
end
shared_table = effil . table ( t )
-- Example #2
t = effil . table ()
for i = 1 , 100 do
t [ i ] = i
end
例 #1 では、通常のテーブルを作成し、それに値を入力して、 effil.table
に変換します。この場合、Effil はすべてのテーブル フィールドをもう一度調べる必要があります。別の方法は、最初にeffil.table
を作成し、その後データをeffil.table
に直接配置する例 #2 です。 2 番目の方法は、この原則に従うことを試みるとかなり速くなります。
時間メトリクスを使用するすべての操作は、ブロッキングまたは非ブロッキングにすることができ、次の API を使用します: (time, metric)
ここで、 metric
は's'
(秒) のような時間間隔であり、 time
間隔の数です。
例:
thread:get()
- スレッドの完了を無限に待ちます。thread:get(0)
- 非ブロッキング取得、スレッドが終了したかどうかを確認して返すだけthread:get(50, "ms")
- 50 ミリ秒間ブロック待機します。利用可能な時間間隔のリスト:
ms
- ミリ秒。s
- 秒 (デフォルト);m
- 分。h
- 時間。すべてのブロッキング操作 (非ブロッキング モードであっても) は中断ポイントです。このような操作でハングしたスレッドは、thread:cancel() メソッドを呼び出すことで中断できます。
local effil = require " effil "
local worker = effil . thread ( function ()
effil . sleep ( 999 ) -- worker will hang for 999 seconds
end )()
worker : cancel ( 1 ) -- returns true, cause blocking operation was interrupted and thread was cancelled
関数の操作 Effil は、 lua_dump
およびlua_load
メソッドを使用して関数をシリアル化および逆シリアル化します。すべての関数の上位値は、通常と同じルールに従って格納されます。関数にサポートされていない型の upvalueがある場合、この関数は Effil に送信できません。その場合、エラーが発生します。
関数 Effil を使用すると、関数環境 ( _ENV
) も保存できます。環境を通常のテーブルとして考えると、Effil は他のテーブルと同じ方法で環境を保存します。ただし、 global _G
保存することは意味がないため、具体的なものがいくつかあります。
_ENV ~= _G
) と等しくない場合にのみ、関数環境をシリアル化して保存します。 effil.thread
スレッド オブジェクトthread:cancel()
およびthread:pause()
の対応するメソッドを使用して一時停止およびキャンセルできます。
中断しようとするスレッドは、明示的と暗黙的の 2 つの実行ポイントで中断される可能性があります。
明示的なポイントはeffil.yield()
です。
local thread = effil . thread ( function ()
while true do
effil . yield ()
end
-- will never reach this line
end )()
thread : cancel ()
暗黙的なポイントは、LUA_MASKCOUNT を指定した lua_sethook を使用して設定される lua デバッグ フック呼び出しです。
暗黙的なポイントはオプションであり、thread_runner.step > 0 の場合にのみ有効になります。
local thread_runner = effil . thread ( function ()
while true do
end
-- will never reach this line
end )
thread_runner . step = 10
thread = thread_runner ()
thread : cancel ()
さらに、スレッドは、ブロッキングまたは非ブロッキングの待機操作でキャンセルできます (ただし、一時停止はできません)。
local channel = effil . channel ()
local thread = effil . thread ( function ()
channel : pop () -- thread hangs waiting infinitely
-- will never reach this line
end )()
thread : cancel ()
キャンセルはどのように行われますか?
スレッドをキャンセルすると、中断ポイントに到達すると、 "Effil: thread is cancelled"
メッセージを含む lua error
が生成されます。これはpcall
使用してこのエラーをキャッチできますが、スレッドは次の中断ポイントで新しいエラーを生成することを意味します。
独自のエラーをキャッチしたいが、キャンセルエラーは渡したい場合は、effil.pcall() を使用できます。
キャンセルされたスレッドのステータスは、キャンセル エラーで終了した場合にのみcancelled
と等しくなります。これは、キャンセル エラーをキャッチした場合、別のエラーが発生した場合、スレッドがcompleted
ステータスまたはfailed
ステータスで終了する可能性があることを意味します。
effil.thread
スレッドを作成する方法です。スレッドは停止、一時停止、再開、キャンセルできます。スレッドを使用したすべての操作は、同期 (オプションのタイムアウトあり) または非同期にすることができます。各スレッドは独自の lua 状態で実行されます。
effil.table
とeffil.channel
使用して、スレッド経由でデータを送信します。スレッドの使用例はこちらをご覧ください。
runner = effil.thread(func)
スレッドランナーを作成します。ランナーは呼び出しごとに新しいスレッドを生成します。
入力: func - Lua 関数
出力: runner - 新しいスレッドを構成して実行するためのスレッドランナーオブジェクト
新しいスレッドを構成して実行できます。
thread = runner(...)
指定された引数を使用してキャプチャされた関数を別のスレッドで実行し、スレッド ハンドルを返します。
input : キャプチャされた関数に必要な任意の数の引数。
出力: スレッド ハンドル オブジェクト。
runner.path
新しい状態の Lua package.path
値です。デフォルト値は親状態からpackage.path
を継承します。
runner.cpath
新しい状態の Lua package.cpath
値です。デフォルト値は親状態からpackage.cpath
を継承します。
runner.step
キャンセルポイント (スレッドを停止または一時停止できるポイント) 間の lua 命令 lua の数。デフォルト値は 200 です。この値が 0 の場合、スレッドは明示的なキャンセル ポイントのみを使用します。
スレッド ハンドルは、スレッドと対話するための API を提供します。
status, err, stacktrace = thread:status()
スレッドのステータスを返します。
出力:
status
- 文字列値はスレッドのステータスを表します。可能な値は、 "running", "paused", "cancelled", "completed" and "failed"
です。err
- エラー メッセージ (存在する場合)。この値は、スレッドのステータス == "failed"
の場合にのみ指定されます。stacktrace
- 失敗したスレッドのスタックトレース。この値は、スレッドのステータス == "failed"
の場合にのみ指定されます。... = thread:get(time, metric)
スレッドの完了を待ち、関数の結果を返すか、エラーの場合は何も返しません。
input : 時間メトリクスの観点からの操作タイムアウト
出力: キャプチャされた関数呼び出しの結果、またはエラーの場合は何も表示されません。
thread:wait(time, metric)
スレッドの完了を待ち、スレッドのステータスを返します。
input : 時間メトリクスの観点からの操作タイムアウト
Output : スレッドのステータスを返します。出力はthread:status()
と同じです
thread:cancel(time, metric)
スレッドの実行を中断します。この関数が呼び出されると、「キャンセル」フラグが設定され、将来 (この関数呼び出しが完了した後でも) スレッドを停止できます。スレッドを確実に停止するには、無限タイムアウトでこの関数を呼び出します。終了したスレッドをキャンセルすると何も行われず、 true
が返されます。
input : 時間メトリクスの観点からの操作タイムアウト
出力: スレッドが停止している場合はtrue
を返し、そうでない場合はfalse
を返します。
thread:pause(time, metric)
スレッドを一時停止します。この関数が呼び出されると、「一時停止」フラグが設定され、将来 (この関数呼び出しが完了した後でも) スレッドを一時停止できます。スレッドが確実に一時停止されるようにするには、無限タイムアウトでこの関数を呼び出します。
input : 時間メトリクスの観点からの操作タイムアウト
出力: スレッドが一時停止された場合はtrue
を返し、そうでない場合はfalse
を返します。スレッドが完了した場合、関数はfalse
を返します
thread:resume()
一時停止したスレッドを再開します。関数はスレッドが一時停止された場合、すぐに再開します。この関数は、完了したスレッドに対しては何も行いません。関数には入力パラメータと出力パラメータがありません。
id = effil.thread_id()
一意の識別子を与えます。
出力:現在のスレッドの一意の文字列id
を返します。
effil.yield()
明示的なキャンセルポイント。関数は現在のスレッドのキャンセルまたは一時停止フラグをチェックし、必要な場合は対応するアクション (スレッドのキャンセルまたは一時停止) を実行します。
effil.sleep(time, metric)
現在のスレッドを一時停止します。
input : 時間メトリクスの引数。
effil.hardware_threads()
実装でサポートされている同時スレッドの数を返します。基本的には std::thread::hardware_concurrency から値を転送します。
出力: 同時ハードウェア スレッドの数。
status, ... = effil.pcall(func, ...)
thread:cancel() 呼び出しによって発生するスレッドキャンセルエラーをキャッチしないことを除いて、標準の pcall とまったく同じように動作します。
入力:
出力:
true
、それ以外の場合はfalse
effil.table
effil スレッド間でデータを交換する方法です。標準の lua テーブルとほぼ同じように動作します。共有テーブルを使用したすべての操作はスレッド セーフです。共有テーブルには、プリミティブ型 (数値、ブール値、文字列)、関数、テーブル、ライト ユーザーデータ、および effil ベースのユーザーデータが保存されます。共有テーブルには、Lua スレッド (コルーチン) や任意のユーザーデータは保存されません。共有テーブルの使用例はこちらをご覧ください。
通常のテーブルと共有テーブルを使用します。通常のテーブルを共有テーブルに格納したい場合、effil は元のテーブルを新しい共有テーブルに暗黙的にダンプします。共有テーブルには常にサブテーブルが共有テーブルとして格納されます。
関数を含む共有テーブルを使用します。関数を共有テーブルに保存すると、effil はこの関数を暗黙的にダンプし、文字列 (およびその上位値) として保存します。すべての関数の上位値は、次のルールに従ってキャプチャされます。
table = effil.table(tbl)
新しい空の共有テーブルを作成します。
input : tbl
-オプションのパラメータです。エントリが共有テーブルにコピーされるのは通常の Lua テーブルのみです。
出力: 空の共有テーブルの新しいインスタンス。 tbl
内容に応じて、空でも空でなくても構いません。
table[key] = value
指定した値でテーブルの新しいキーを設定します。
入力:
key
- サポートされているタイプの任意の値。サポートされているタイプのリストを参照してくださいvalue
- サポートされているタイプの任意の値。サポートされているタイプのリストを参照してくださいvalue = table[key]
指定したキーを持つテーブルから値を取得します。
input : key
- サポートされているタイプの任意の値。サポートされているタイプのリストを参照してください
出力: value
- サポートされているタイプの任意の値。サポートされているタイプのリストを参照してください
tbl = effil.setmetatable(tbl, mtbl)
新しいメタテーブルを共有テーブルに設定します。標準の setmetatable に似ています。
入力:
tbl
メタテーブルを設定する共有テーブルである必要があります。mtbl
通常のテーブル、またはメタテーブルとなる共有テーブルである必要があります。通常のテーブルの場合、 effil は新しい共有テーブルを作成し、 mtbl
のすべてのフィールドをコピーします。共有テーブルからメタテーブルを削除するには、 mtbl
nil
に設定します。 Output : 標準の Lua setmetatableメソッドと同様に、新しいメタテーブル値を含むtbl
返すだけです。
mtbl = effil.getmetatable(tbl)
現在のメタテーブルを返します。標準の getmetatable と同様
input : tbl
共有テーブルである必要があります。
出力: 指定された共有テーブルのメタテーブルを返します。返されるテーブルの型は常にeffil.table
です。デフォルトのメタテーブルはnil
です。
tbl = effil.rawset(tbl, key, value)
メタメソッド__newindex
を呼び出さずにテーブル エントリを設定します。標準のRawsetと同様
入力:
tbl
共有テーブルです。key
- オーバーライドするテーブルのキー。キーは、サポートされている任意のタイプにすることができます。value
- 設定する値。値には、サポートされている任意のタイプを指定できます。出力: 同じ共有テーブルtbl
を返します。
value = effil.rawget(tbl, key)
メタメソッド__index
を呼び出さずにテーブル値を取得します。標準の rawget と同様
入力:
tbl
共有テーブルです。key
- 特定の値を受け取るテーブルのキー。キーには、サポートされている任意のタイプを使用できます。出力: 指定されたkey
の下に格納されている必要なvalue
を返します
effil.G
グローバルな事前定義された共有テーブルです。このテーブルは、どのスレッド(どの Lua 状態)にも常に存在します。
effil = require " effil "
function job ()
effil = require " effil "
effil . G . key = " value "
end
effil . thread ( job )(): wait ()
print ( effil . G . key ) -- will print "value"
result = effil.dump(obj)
effil.table
通常の Lua テーブルに変換します。
tbl = effil . table ({})
effil . type ( tbl ) -- 'effil.table'
effil . type ( effil . dump ( tbl )) -- 'table'
effil.channel
、effil スレッド間でデータを順次交換する方法です。これにより、あるスレッドからメッセージをプッシュしたり、別のスレッドからメッセージをポップしたりすることができます。チャネルのメッセージは、サポートされているタイプの値のセットです。チャネルを使用したすべての操作はスレッド セーフです。チャネルの使用例はこちらをご覧ください
channel = effil.channel(capacity)
新しいチャンネルを作成します。
input : チャネルのオプションの容量。 capacity
が0
またはnil
の場合、チャネルのサイズは無制限です。デフォルトの容量は0
です。
Output : チャネルの新しいインスタンスを返します。
pushed = channel:push(...)
メッセージをチャネルにプッシュします。
input : サポートされている型の任意の数の値。複数の値は 1 つのチャネル メッセージとしてみなされるため、チャネルへの 1 回のプッシュにより容量が 1 つ減少します。
出力: value(-s) がチャネル容量に適合する場合、 pushed
true
に等しく、そうでない場合はfalse
に等しくなります。
... = channel:pop(time, metric)
チャンネルからのポップメッセージ。チャネルから値 (-s) を削除して返します。チャネルが空の場合は、値が表示されるまで待ちます。
input : 時間メトリックに関する待機タイムアウト (チャネルが空の場合にのみ使用されます)。
Output : 単一の channel:push() 呼び出しによってプッシュされた可変量の値。
size = channel:size()
チャネル内の実際のメッセージ量を取得します。
出力: チャネル内のメッセージの量。
Effil は、 effil.table
およびeffil.channel
(およびキャプチャされた上位値を持つ関数) 用のカスタム ガベージ コレクターを提供します。これにより、複数のスレッドでテーブルとチャネルの循環参照を安全に管理できます。ただし、余分なメモリ使用量が発生する可能性があります。 effil.gc
effil ガベージ コレクターを構成するメソッドのセットを提供します。ただし、通常は設定する必要はありません。
ガベージ コレクターは、effil が新しい共有オブジェクト (テーブル、チャネル、およびキャプチャされた上位値を持つ関数) を作成するときにその作業を実行します。各反復 GC ではオブジェクトの量がチェックされます。割り当てられたオブジェクトの量が多くなると、特定のしきい値 GC がガベージ コレクションを開始します。しきい値はprevious_count * step
として計算されます。ここで、 previous_count
- 前回の反復でのオブジェクトの量 (デフォルトでは100 )、 step
ユーザーが指定した数値係数 (デフォルトでは2.0 ) です。
たとえば、GC step
が2.0
で、割り当てられたオブジェクトの量が120
(前回の GC 反復後に残ったもの) の場合、割り当てられたオブジェクトの量が240
に等しくなったときに GC はガベージの収集を開始します。
各スレッドは、独自のガベージ コレクターを備えた個別の Lua 状態として表されます。したがって、オブジェクトは最終的には削除されます。 Effil オブジェクト自体も GC によって管理され、デシリアライザー フックとして__gc
userdata メタメソッドを使用します。オブジェクトを強制的に削除するには:
collectgarbage()
呼び出します。effil.gc.collect()
を呼び出します。effil.gc.collect()
ガベージ コレクションを強制しますが、すべての effil オブジェクトの削除が保証されるわけではありません。
count = effil.gc.count()
割り当てられた共有テーブルとチャネルの数を表示します。
出力: 割り当てられているオブジェクトの現在の数を返します。最小値は 1 で、 effil.G
は常に存在します。
old_value = effil.gc.step(new_value)
GC メモリ ステップ乗数を取得/設定します。デフォルトは2.0
です。割り当てられたオブジェクトの量がstep
時間内に増加すると、GC によって収集がトリガーされます。
input : new_value
は、設定するステップのオプションの値です。 nil
の場合、関数は現在の値を返すだけです。
出力: old_value
はステップの現在値 ( new_value == nil
の場合) または以前の値 ( new_value ~= nil
の場合) です。
effil.gc.pause()
GC を一時停止します。ガベージコレクションは自動的には実行されません。関数には入力も出力もありません
effil.gc.resume()
GCを再開します。自動ガベージ コレクションを有効にします。
enabled = effil.gc.enabled()
GC 状態を取得します。
出力: 自動ガベージ コレクションが有効な場合はtrue
を返し、そうでない場合はfalse
を返します。デフォルトではtrue
を返します。
size = effil.size(obj)
Effil オブジェクト内のエントリの数を返します。
input : obj
共有テーブルまたはチャネルです。
出力: 共有テーブル内のエントリの数またはチャネル内のメッセージの数
type = effil.type(obj)
スレッド、チャネル、テーブルはユーザーデータです。したがって、 type()
任意の型のuserdata
を返します。型をより正確に検出したい場合は、 effil.type
使用します。通常のtype()
と同様に動作しますが、effil 固有のユーザーデータを検出できます。
input : obj
任意の型のオブジェクトです。
出力: 型の文字列名。 obj
が Effil オブジェクトの場合、関数はeffil.table
のような文字列を返し、それ以外の場合は lua_typename 関数の結果を返します。
effil . type ( effil . thread ()) == " effil.thread "
effil . type ( effil . table ()) == " effil.table "
effil . type ( effil . channel ()) == " effil.channel "
effil . type ({}) == " table "
effil . type ( 1 ) == " number "