Kinx ライブラリ - Isolate (マルチスレッド)
はじめに
「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。言語はライブラリが命。ということでライブラリの使い方編。
今回は Isolate です。お待ちかねマルチスレッドです。そして Native Thread です。Isolate という名前からわかるように、スレッドモデルとしては各スレッド独立して動作し、メモリを共有させません。それによって安全性を高めるという選択肢です。
- 参考
- 最初の動機 ... スクリプト言語 KINX(ご紹介)
- 個別記事へのリンクは全てここに集約してあります。
- リポジトリ ... https://github.com/Kray-G/kinx
- Pull Request 等お待ちしております。
- 最初の動機 ... スクリプト言語 KINX(ご紹介)
Isolate
マルチスレッドのモデル
C/C++ みたいなスレッドのメモリ共有モデルは危険が多すぎて大変です。レースコンディションの罠に細心の注意を払い、排他制御を確実に行いながら、でもやっぱり簡単にデッドロックしたりして。マルチスレッドと安全性の闘いはいまだに続いています。
Ruby や Python のスレッドモデルは安全ですが、GIL (Global Interpreter Lock) によって並列性が制限されてしまうのが弱点です。
Kinx でもこの壁に対する チャレンジをしましょう。Ruby は過去のしがらみで GIL から未だ解放されていませんが、もうそろそろ次のステージに進みそうではあります。尚 Ruby では Guild みたいです Ractor となりましたが、Kinx では Isolate
になっています。
Guild は Ruby 3 で Ractor として(実験的機能ですが)導入されました!
ということで Kinx では Isolate です。完全に独立した ネイティブ・スレッド です。情報のやり取りは Integer, Double, String に限られます。なのでオブジェクトをやり取りしたい場合は Serialize, Deserialize の仕組みを用意する必要があります。
また、コンパイル・フェーズだけはリエントラントにできていません。なので、コンパイル・フェーズはロックされて順に処理されることになります。
Isolate オブジェクト
ざっくり Isolate オブジェクトは以下のように使用します。
-
new Isolate(src)
で Isolate オブジェクトを作成する。この時点ではまだ実行されない。 -
Isolate#run()
でコンパイル & 実行が行われる。戻り値は Isolate オブジェクトのthis
。 -
Isolate#join()
で実行したスレッドの終了を待つ。 - 尚、メインスレッドが終了すると、全てのスレッドが 問答無用で 終了する。
- なので、終了をコントロールする場合は後述するデータ転送などの方法を使って同期させ、メインスレッドで正しく
join
する。
- なので、終了をコントロールする場合は後述するデータ転送などの方法を使って同期させ、メインスレッドで正しく
サンプル
スレッドの作成
まずはサンプルを見てみましょう。Isolate クラスのコンストラクタに渡すのは 文字列 です。Raw 文字列で書くとちゃんとプログラムコードのように見えていい感じ。が、一つだけ落とし穴があります。
- Raw 文字列内の
%{...}
は Raw 文字列に対する inner-expression と解釈されてしまう。
ということで Raw 文字列内での %{...}
の使用は避けます。以下の例ではフォーマッタ・オブジェクトを作成して %1%
に値を直接埋め込むようにしています。ちょっと JIT っぽい。
var fibcode = %{
function fib(n) {
return n < 3 ? n : fib(n-2) + fib(n-1);
}
v = fib(%1%);
var mutex = new Isolate.Mutex();
mutex.lock(&() => System.println("fib(%1%) = ", v));
};
34.downto(1, &(i, index) => new Isolate(fibcode % i).run())
.each(&(thread, i) => { thread.join(); });
表示部分だけ変にグチャグチャになるのを避けるためにロックしておきます。
fib(15) = 987
fib(10) = 89
fib(20) = 10946
fib(3) = 3
fib(11) = 144
fib(21) = 17711
fib(4) = 5
fib(9) = 55
fib(23) = 46368
fib(16) = 1597
fib(14) = 610
fib(8) = 34
fib(2) = 2
fib(24) = 75025
fib(26) = 196418
fib(28) = 514229
fib(29) = 832040
fib(7) = 21
fib(30) = 1346269
fib(25) = 121393
fib(5) = 8
fib(13) = 377
fib(12) = 233
fib(19) = 6765
fib(22) = 28657
fib(18) = 4181
fib(17) = 2584
fib(6) = 13
fib(27) = 317811
fib(31) = 2178309
fib(1) = 1
fib(32) = 3524578
fib(33) = 5702887
fib(34) = 9227465
スレッドの特性として、実行するたびに順序が変わったります。
fib(10) = 89
fib(19) = 6765
fib(14) = 610
fib(11) = 144
fib(26) = 196418
fib(17) = 2584
fib(21) = 17711
fib(20) = 10946
fib(9) = 55
fib(13) = 377
fib(28) = 514229
fib(18) = 4181
fib(30) = 1346269
fib(31) = 2178309
fib(7) = 21
fib(3) = 3
fib(8) = 34
fib(4) = 5
fib(25) = 121393
fib(16) = 1597
fib(22) = 28657
fib(23) = 46368
fib(12) = 233
fib(27) = 317811
fib(29) = 832040
fib(15) = 987
fib(2) = 2
fib(5) = 8
fib(1) = 1
fib(6) = 13
fib(32) = 3524578
fib(24) = 75025
fib(33) = 5702887
fib(34) = 9227465
スレッドの終了
Isolate の実行が最後まで行くとスレッドは終了します。
スレッドが返した値は join
の復帰値として取得できます。
var r = new Isolate(%{ return 100; }).run().join();
System.println("r = %d" % r);
r = 100
データ転送 - Isolate.send/receive/clear
単純なデータ転送用に、Isolate.send(name, data)
と Isolate.receive(name)
を用意してあります。name
によってバッファが区別され、同じ name
で情報を受け取れる。以下のような仕様になっています。
-
name
は省略可能。省略した場合は"_main"
が指定されたことと同じ。 -
data
は Integer, Double, String しかサポートしていない。- そのため、オブジェクトを受け渡す場合は文字列化して送信し、受信側で再構築する必要がある。
-
Isolate.clear(name)
でバッファの情報をクリアする。- 逆に、
Isolate.clear(name)
をしなければ情報はクリアされない。Isolate.receive(name)
で何度も同じ情報が取得される。
- 逆に、
ミューテックス - Mutex
Mutex オブジェクトは Isolate.Mutex
クラス・オブジェクトとして構築します。ちなみにプロセス内であっても同じ Mutex を取得するために名前で区別して構築します。
var m = new Isolate.Mutex('mtx');
同じ名前が指定された場合、同じ Mutex オブジェクトが構築される。名前を省略した場合は "_main"
を指定したものとみなされます。
Mutex オブジェクトは Mutex#lock(func)
メソッドで使用し、コールバックとして指定した func
関数は Mutex がロックされた状態でコールバックされます。
var m = new Isolate.Mutex('mtx');
m.lock(&() => {
// locked
...
});
条件変数 - Condition
条件変数を使えます。ロックした Mutex オブジェクトと一緒に使います。Condition#wait()
にロックした Mutex オブジェクトを渡すと、Mutex をアンロックした上で待機状態に入ります。その状態で他スレッドから Condition#notifyAll()
されると、ロックが取れればロックを取って待機状態から復帰してきます。ロックが取れなければロックが取れるまでそのまま待機します。
よくある Condition#notifyOne()
はサポートしません。どこ見ても**「使わないほうが良い」**って書いてありますし。全て notifyAll()
を使い、全てのスレッドで条件確認させてください。
var cond = %{
var m = new Isolate.Mutex('mtx');
var c = new Isolate.Condition('cond');
m.lock(&() => {
var i = 0;
while (i < 10) {
System.println("Wait %1%");
c.wait(m);
System.println("Received %1%");
++i;
}
System.println("Ended %1%");
});
};
var ths = 34.downto(1, &(i, index) => new Isolate(cond % i).run());
System.sleep(1000);
var c = new Isolate.Condition('cond');
34.times(&(i) => {
System.println("\nNotify ", i);
c.notifyAll();
System.sleep(500);
});
ths.each(&(thread) => {
thread.join();
});
名前付きミューテックス - NamedMutex
プロセス間で共有する Mutex オブジェクトです。Isolate.NamedMutex
クラス・オブジェクトとして構築するが、使い方は通常の Mutex と同じ。
ただ、名前が Isolate.NamedMutex
で良いのか不明。プロセス間で排他制御できるので、対象が Isolate に収まってません。Process.NamedMutex
とか System.NamedMutex
のほうが良いか? ご意見求む。
var mtx = Isolate.NamedMutex('ApplicationX');
mtx.lock(&() => {
...
});
他プロセスと排他したいときに使います。
シリアライズ・デシリアライズ
現時点ではシリアライズ・デシリアライズをサポートするための機能がありません。自力で行う必要があります。本当は何らかの支援する仕組みを入れたいところですので、何か考えようとは思っています。
一先ずの基本戦略は、文字列化して再オブジェクト化、です。単純なプリミティブ要素のみを持つ JSON オブジェクトであれば、JSON.stringify
と JSON.parse
を使って実現できます。というか、文字列中に直接 toJsonString()
した形で突っ込んでしまうのもアリです。男らしい JIT なアプローチ。
var t = %{
var o = %1%;
System.println(["Name = ", o.name, ", age = ", o.age].join(''));
};
var o = {
name: "John",
age: 29,
};
new Isolate(t % o.toJsonString()).run().join();
Name = John, age = 29
動的に渡したい場合は、デシリアライズ用のコードを自分で書く必要があります。
var t = %{
var o;
do {
o = Isolate.receive();
} while (!o);
System.println("Received message.");
o = JSON.parse(o);
System.println(["Name = ", o.name, ", age = ", o.age].join(''));
};
var o = {
name: "John",
age: 29,
};
var th = new Isolate(t).run();
Isolate.send(o.toJsonString());
th.join();
Received message.
Name = John, age = 29
おわりに
色々と実行時コンテキストに依存させ、C の関数ベースでリエントラントにすることで実行エンジンを並列に干渉させずに実行させるようにデザインしてみました。それによって GIL を使わない、完全ネイティブな並列処理を実現すべく頑張ってみました。見落としが無ければ...。
いやー、正直全く見落としていない、と自信を持って言えるはずもなく(開発者ならわかりますよね...)、何かあれば修正しなければならないでしょうが、今のところ問題が起きていません。dll 内とかも一応見てみたつもり。
チャレンジとしてはまずはここからスタート。Isolate 自体はマルチコア向けのマルチスレッド機構を何とか入れたいなー、と思って入れてみたところなので、未だ発展途上ではあります。頑張りましょう。
ではまた次回。