SyntaxHighlighter

2013年12月27日金曜日

SevenZipRuby 作成メモ 3 - マルチスレッド

SevenZipRuby 作成メモ 3 - マルチスレッド

7z ファイルを読み書きする Ruby gem ライブラリである seven_zip_ruby の作成メモその 3 です。
ひとまずこれでメモは終了し、久し振りに mruby の世界へ戻ろうと思います。

最後のメモはマルチスレッドや GVL (Global VM Lock) についてです。

概要

SevenZipRuby では、LZMA や BZIP2 圧縮の 7z ファイルを作る場合、デフォルトでマルチスレッドによる圧縮をサポートしています。
今回は、このマルチスレッドへの対応について、メモしておきます。

また rb_thread_call_without_gvl の使い方についても書いておきます。
ただし、(他の記事もそうですが) 今回の記事はあまり正しいかどうか、あまり自信がありません。

Ruby のマルチスレッド処理と GVL (Global VM Lock)

Ruby には Thread クラスがあり、手軽にマルチスレッドプログラミングを楽しむことができます。
ただし、MRI (CRuby) では複数の CPU Core がある環境で複数のスレッドを作成したとしても、同時に実行されるスレッドは高々一つになります。
これは GVL というグローバルな Mutex があり、これを取得しているスレッドしか動作しないように実装されているためです。

CPython でも同様な実装らしく GIL (Global Interpreter Lock) というものがあります。
詳しくは Wikipedia の GIL の項目などを見てください。

さて、GVL は、現状スレッドセーフに書かれていない CRuby の各関数や SevenZipRuby のような拡張機能が、マルチスレッド環境下で (そこそこに) 動くように守ってくれています。
GVL があることで、C の拡張機能を書いたときに、特定の Ruby の C API を呼んだときなど、こちらが明示的に指定した箇所でしかスレッドの切り替えが起こらないようになっています。
例えば、以下のような C 拡張機能のコードでは、スレッドの切り替えが起こりません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// C 拡張機能
static VALUE test(VALUE self)
{
    // 実行に 1s ぐらいかかる処理など
    //  e.g. 複雑な数値計算
    SleepEx(1000, FALSE);  // 例としてとりあえず 1s 待つ
    return Qnil;
}
 
void Init_hoge(void)
{
    VALUE cls = rb_define_class("Hoge");
    rb_define_method(cls, "test", RUBY_METHOD_FUNC(test), 0);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Ruby スクリプト
require("hoge")
 
# 別スレッドで 0.1s ごとに "check" を表示
Thread.start do
  loop do
    sleep 0.1
    puts "check"
  end
end
 
a = Hoge.new
# 以下の test メソッド実行中は、
# スレッドの切り替えが起こらない
#  => "check" の出力は止まる
a.test

この例の場合、Hoge#test の実行中は、"check" の出力が止まります。
これは、Ruby の C 拡張関数の実行中は GVL は取得されたままで解放されることがないため、他のスレッドが実行されたりしないためです。

ところで、VC++ 2010 などで試す場合、上の test 関数を以下のように定義すると、"check" が 0.1s ごとに出力されます。

1
2
3
4
5
static VALUE test(VALUE self)
{
    Sleep(1000);
    return Qnil;
}

これは、Ruby 側に細工があり、上記の例では Sleep 関数は Windows API ではなく、適切に GVL を解放してから待ちに入る rb_w32_Sleep が代わりに呼ばれるためです。
SleepEx であれば、このようなことは起こらないので、試す場合はこちらを使うとよいでしょう。

rb_thread_call_without_gvl による GVL の解放

先に挙げた SleepEx の例で、"check" が 0.1s ごとに出力されるようにするには、rb_thread_call_without_gvl を利用します。

この関数は、引数で指定した関数を、GVL を解放した状態で呼んでくれます。
そのため、上述した test 関数を以下のように書くと、"check" が 0.1s ごとに出力されるようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void *slow_func(void *data)
{
    // この関数は GVL を解放した状態で実行される
    //  => 他の Ruby スレッドをブロックしない
    SleepEx(1000, FALSE);
    return NULL;
}
 
static void cancel_func(void *data)
{
    // ここでは、slow_func を中断するための処理を書く。
    // 例えば slow_func 内で定期的に「中断用フラグ」を
    // チェックするようにしておき、この関数内でその
    // フラグを true にする。
    // Ruby 処理系は、slow_func を強制終了させたいときに、
    // cancel_func を呼んでくる。
}
 
static VALUE test(VALUE self)
{
    // slow_func を GVL を解放した状態で呼ぶ。
    rb_thread_call_without_gvl(slow_func, NULL, cancel_func, NULL);
    return Qnil;
}

このように書くことで、slow_func は GVL を解放した状態で実行されます。

ただし、注意点として、GVL を解放した状態では、ほぼすべての Ruby 関係の API を呼ぶことはできない というものがあります。
上記の slow_func の中では、例えば rb_gv_get でグローバル変数にアクセスしたりはできません。そのため、重い数値計算など、時間がかかる処理に限定して行うとよいでしょう。

ちなみに、Windows では Sleeprb_w32_Sleep になるので、上の例で SleepEx の代わりに Sleep を呼んでしまうと、GVL の扱いがおかしくなり、SEGV することになります。

7z.dll の呼び出し

以前に書いたように、7z.dll は内部で独自にスレッドを作成します。
そのため、以下のような方針で拡張機能を作ることにりました。

  • 7z.dll が作ったスレッド内で呼び出される Ruby のコールバック関数は、そこで直接処理しない
  • その処理をするために、別途 Ruby のスレッドを作り、イベントループのような動作をさせる
  • Ruby スレッドが 7z.dll のイベント待ちをする際は、条件変数で CPU を消費しないようにして待つ

イベントループの作成

概念的には、以下のようなループで 7z.dll のコールバック処理を行います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
VALUE runProtectedRubyAction(VALUE p)
{
    typedef std::function<void ()> func_type;
    func_type *action = reinterpret_cast<func_type *>(p);
    (*action)();
    return Qnil;
}
 
void rubyEventLoop()
{
    g_action_mutex.lock();
    while(g_event_loop_running){
        g_action_mutex.unlock();
 
        // 7z.dll からのイベント登録を待つ
        std::function<void ()> *action = wait_for_event();
 
        int status = 0;
        // 登録された関数を実行する
        rb_protect(runProtectedRubyAction, reinterpret_cast<VALUE>(action), &status);
 
        g_action_mutex.lock();
        g_action = nullptr;
        g_action_cond_var.broadcast();
    }
    g_action_mutex.unlock();
}

この rubyEventLoop は Ruby のスレッドとして rb_thread_create から実行されます。
この中では、g_action_mutexg_action をガードしながら、g_action_cond_var の知らせを wait_for_event で待ちます。

イベント登録側は以下のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
template<typename T>
void runRubyAction(T t)
{
    g_action_mutex.lock();
 
    // イベントが空くまで待つ
    while(g_action){
        g_action_cond_var.wait(&g_action_mutex);
    }
 
    std::function<void ()> action = t;
    // 空いたので登録
    g_action = &action;
 
    g_action_cond_var.broadcast();
 
    // 処理されるまで待つ
    while(g_action == &action){
        g_action_cond_var.wait(&g_action_mutex);
    }
 
    g_action_mutex.unlock();
}
 
// 以下のように使う
void hoge()
{
    // 関数 hoge 自体は 7z.dll から呼ばれる
    // そのため、7z.dll が作った独自スレッドから呼ばれるかもしれない
    runRubyAction([&](){
        // この中では Ruby の関数を好きなように呼べる
        // これらの処理は、Ruby のスレッドの中から呼ばれることになる
        VALUE value = rb_funcall(gHoge, rb_intern("new"), 0);
    });
}

実際にはエラー処理などがありますが、概要としては以上のように Ruby 用のスレッドを用意して 7z.dll の処理を行っています。

イベント待ち

イベント待ちの部分では、適切に GVL を解放した上で Mutex の wait を呼ぶ必要があります。
上記の例の wait_for_event では、以下のように待つ必要があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void *wait_for_event_without_gvl(void *ptr)
{
    while(!g_action){
        g_action_cond_var.wait(&g_action_mutex);
    }
    return nullptr;
}
 
std::function<void ()> *wait_for_event()
{
    g_action_mutex.lock();
 
    rb_thread_call_without_gvl(wait_for_event_without_gvl, nullptr, nullptr, nullptr);
 
    std::function<void ()> *action = g_action;
    g_action_mutex.unlock();
    return action;
}

このように GVL を解放して Mutex の wait を呼ばないと、wait を呼んだ途端に他の Ruby スレッドが GVL を取得することができなくなってしまうので、ハングします。

というわけで、マルチスレッドの場合は GVL をきちんと考慮して書く必要があります。

私自身もあまり深く分かっているわけではないですが、半年後ぐらいには seven_zip_ruby の実装を忘れてしまいそうなので、メモしておきました。

2013年12月15日日曜日

SevenZipRuby 作成メモ 2 - C++ での Ruby 拡張ライブラリのコーディング

SevenZipRuby 作成メモ 2 - C++ での Ruby 拡張ライブラリのコーディング

7z ファイルを読み書きする Ruby gem ライブラリである seven_zip_ruby も、無事に rubygems で公開しました。
ダウンロード数から察するに、あまり必要とされていないような気がしないでもないですが、以下にリンクをはっておきます。

今回は、 SevenZipRuby の作成メモ 2 として、 C++ での Ruby 拡張ライブラリのコーディングについて書いておきます。

概要

C++ での Ruby 拡張ライブラリのコーディングについては、 tueda の日記 - (Cではなく)C++でRubyの拡張ライブラリを作るには などが非常に分かりやすく書かれているので、とても参考になると思います。
また、 seven_zip_ruby では使いませんでしたが、 C++ で Ruby 拡張ライブラリを作成するための Rice というライブラリも公開されており、 Rice を使用して Ruby の拡張機能を C++ で作成するなどのページが参考になると思います。

今回は、私が個人的に気をつける必要があると感じた、例外まわりの実装などについて書いておこうと思います。

Ruby の例外機構

Ruby では、 raise "hoge"raise SyntaxError, "invalid syntax" のように例外を発生させ、それを外側のメソッドなどで begin, rescue, ensure を使って捕捉することができます。

この仕組みは、 MRI (CRuby) の場合、 setjmplongjmp によって実現されています。

setjmplongjmp

これらは C 言語で大域脱出を実現するための機構であり、例えば以下のようなことができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
jmp_buf gBuf;
 
void sub_function(void)
{
    if (error_occured()){
        longjmp(gBuf, 1);
        /* ここには来ない */
    }
 
    /* 他の処理 */
}
 
int main(int argc, char *argv[])
{
    if (setjmp(gBuf) == 0){
        sub_function();
        /* sub_function 内で longjmp が呼ばれると、ここには来ない */
    }else{
        /* sub_function 内で longjmp が呼ばれると、ここに来る */
    }
 
    return 0;
}

上記の例のように、 setjmp が呼ばれると、将来その関数の先で longjmp が呼ばれたときのために、「戻り先」を示すための CPU 依存の情報 (実行コンテキスト) を引数 gBuf に保存します。
その後、 longjmp が呼ばれると、引数で渡された実行コンテキストから、 setjmp を呼んだときの「戻り先」を求め、その状態に戻ります。

とてもとても大雑把に書くと、ネストされた関数内から外側の関数へ一度に移動できる goto のようなものです。
Java にはラベル付き break がありますが、「関数をまたいで使えるラベル付き break 」のようなイメージです。

より詳細な (そして正確な) 説明としては longjmpと例外などがとても分かりやすいです。

しかし、 goto などと異なり、 setjmp, longjmp は C++ で使う際には「デストラクタとの併用が困難」という大きな注意点があります。

C++ と setjmp, longjmp

例えば以下のようなコードを考えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Test
{
  public:
    Test(){ std::cout << "Constructor" << std::endl; }
    ~Test(){ std::cout << "Destructor" << std::endl; }
};
 
void sub_function()
{
    Test test;  // ~Test() が呼ばれるかチェック
    // 必ず例外を投げる
    throw std::invalid_argument("hoge");
}
 
int main(int argc, char *argv[])
{
    try{
        sub_function();
    }catch(const std::invalid_argument &e){
        std::cout << "Error!" << std::endl;
    }
    return 0;
}

この場合、 Constructor, Destructor, Error! と表示され、デストラクタ ~Test() が呼ばれていることが分かります。

一方で、以下のようなコードの場合、デストラクタは呼ばれません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Test
{
  public:
    Test(){ std::cout << "Constructor" << std::endl; }
    ~Test(){ std::cout << "Destructor" << std::endl; }
};
 
jmp_buf gBuf;
 
void sub_function()
{
    Test test;  // ~Test() が呼ばれるかチェック
    // 必ず例外を投げる
    longjmp(gBuf, 1);
}
 
int main(int argc, char *argv[])
{
    if (setjmp(gBuf) == 0){
        sub_function();
    }else{
        std::cout << "Error!" << std::endl;
    }
    return 0;
}

このように、 longjmp は強制的に setjmp を呼んだときのスタック状態に戻すので、 setjmp, longjmp の間で呼ばれるべきデストラクタは一切呼ばれません。
ここはとても重要なポイントです。

例外を考慮した C++ による Ruby の拡張ライブラリの作成

既に書いたように、 Ruby の例外機構は setjmp, longjmp を使って実現されています。
大雑把に書くと、 Ruby コード中の begin の部分で setjmp され、 raise Hoge の部分で longjmp されています。

そのため、 C++ で Ruby の拡張ライブラリを書く場合、前述したようなデストラクタの呼び出しがされない場合があることに注意が必要です。
例えば、 std::vector なんかを使っていると、簡単にメモリーリークが発生します。

1
2
3
4
5
6
7
8
9
10
// メモリーリークが発生する例
VALUE some_method(VALUE self, VALUE num)
{
    std::vector<unsigned int> buf(10);
    // 以下の行で to_i 呼び出し時に例外が発生すると、
    // この関数から longjmp で脱出するので vector のデストラクタは
    // 呼ばれない
    VALUE size = rb_funcall(num, rb_intern("to_i"), 0);
    // ...
}

C++ で書く場合は、こういったことを考慮して書く必要があります。
例えば、以下のように書く必要があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// メモリーリークが発生しない例
VALUE sub_function(VALUE arg)
{
    // この関数では C++ のクラスを使わない
    // (ので longjmp による影響を考慮しなくてよい)
    VALUE size = rb_funcall(num, rb_intern("to_i"), 0);
    return size;
}
 
VALUE some_method(VALUE self, VALUE num)
{
    std::vector<unsigned int> buf(10);
 
    int state = 0;
    // rb_protect は、中で例外が発生した場合、それを外に伝搬せずに
    // state の値として例外があったことを示す。
    VALUE size = rb_protect(sub_function, num, &state);
    if (state != 0){
        // to_i で例外が発生したので何らかの処理をする。
        // 例: 戻り値としてエラーが分かる値を返し、この関数の外で buf の
        //      デストラクタが呼ばれてから、改めて Ruby の例外を投げる。
    }
    // ...
}

上記のように書けば、 Ruby の例外による longjmp の範囲を rb_protect で呼び出した sub_function の内部に閉じ込めることができます。

でも、それぞれの Ruby メソッドの呼び出しについて関数を分けるのは面倒です。

ラムダ関数の活用

なので、 C++ のラムダ関数を使ってみることにします。

例えば、以下のような関数を作っておいて、 rb_protect に渡すようにします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template<typename T>
VALUE run_functor_for_protect(VALUE p)
{
    T *t = reinterpret_cast<T*>(p);
    return (*t)();
}
 
template<typename T>
VALUE run_protect(T func)
{
    int state = 0;
    VALUE ret = rb_protect(run_functor_for_protect<T>, reinterpret_cast<VALUE>(&func), &state);
    if (state != 0){
        // エラー時の処理
    }
    return ret;
}
 
VALUE some_method(VALUE self, VALUE num)
{
    std::vector<unsigned int> buf(10);
    VALUE size = run_protect([&](){ return rb_funcall(num, rb_intern("to_i"), 0); });
}

こんな感じのテンプレート関数を作っておくと、あとはラムダ関数を使うことで、割と楽に Ruby の関数を呼び出すことができます。

C 言語と C++ の関数呼び出し規約におけるコンパイラ依存性

厳密には、上記の方法はコンパイラに依存しており、うまく動かないこともあります。

run_protect 関数の中で、 C 言語の関数である rb_protect を呼んでおり、その第一引数として run_functor_for_protect<T> のように C++ の関数ポインタを渡しています。

この部分がコンパイラに依存しており、うまく動かないかもしれない部分です。たぶん。

本来であれば、 C 言語の関数に関数ポインタを渡す場合は、 extern "C" 宣言された関数でなければなりません。

上の例のコードがうまく動くのは、「C 言語と C++ の関数の呼び出し規約が一致しているとき」です。
例えば「C 言語の関数の呼び出し規約は cdecl なのに、 C++ の関数の呼び出し規約は stdcall」みたいなコンパイラがあると、当然うまく動作しません。ただし、そんなコンパイラがあるのかは知りません。

C++ の仕様としては、こういう場合は extern "C" を付けることで、呼び出し規約 (と関数の名前修飾) が C 言語と同じになることが保証されていますが、テンプレート関数には extern "C" を付けることはできません。
名前修飾はともかくとして、呼び出し規約だけは C 言語と同じになるような書き方をしたいのですが、標準の書き方ではなさそうな感じです。

function クラスの利用

そのため、ラムダ関数とともに C++11 で追加された function クラスを使うことにします。

今回の場合では以下のようにすると、コンパイラ依存なコードにならないでしょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// この run_functor_for_protect は extern "C" 宣言できる
extern "C" VALUE run_functor_for_protect(VALUE p)
{
    typedef std::function<VALUE ()> func_type;
    func_type *t = reinterpret_cast<func_type*>(p);
    return (*t)();
}
 
template<typename T>
VALUE run_protect(T func)
{
    typedef std::function<VALUE ()> func_type;
    func_type f = func;
 
    int state = 0;
    VALUE ret = rb_protect(run_functor_for_protect, reinterpret_cast<VALUE>(&f), &state);
    if (state != 0){
        // エラー時の処理
    }
    return ret;
}
 
VALUE some_method(VALUE self, VALUE num)
{
    std::vector<unsigned int> buf(10);
    VALUE size = run_protect([&](){ return rb_funcall(num, rb_intern("to_i"), 0); });
}

以上のように、 C++ で Ruby の拡張ライブラリを書くのは、割と面倒です。
seven_zip_ruby のように、呼び出し先の DLL が生の C++ を必要とするような環境でもない限り、 C で書くか、 Rice などのライブラリを使うのが楽でしょう。

2013/12/23 function に関する記述を追加