2011年6月30日木曜日

グローバルインタプリタロックを無くす方法

原文はこちら: Global Interpreter Lock, or how to kill it

EuroPythonで私(Armin Rigo)のライトニングトークを聴いた人は、突然知ったでしょう。我々はグローバルインタプリタロック---悪名高いGIL、複数のスレッドが実際にPythonコードへ干渉するのを防ぐCPythonのものです ---を取り除く計画を持っています。

それは画期的なものではありません。なぜならJythonは既にGILを取り除くことに対応しています。
Jythonは非常に慎重に全ての変更可能な組み込み型にロックを追加し、それらに対して、効率的になるよう基盤にあるJavaプラットフォームを頼っています。(結果は非常に慎重にCPythonで同様なロックを追加するより速いです)
"非常に慎重"は本当の本当に慎重を意味します。
例えば、'dict1.update(dict2)' はdict1とdict2の両方をロックする必要があります。しかし、あなたが安易にしてしまうなら、'dict2.update(dict1)'はデッドロックを生じる可能性があります。

PyPy、CPython、IronPythonとすべてはGILを持っています。
しかし、我々はPyPyに対して、Software Transactional Memory(ソフトウェアトランザクショナルメモリ)に基づく、Jythonとは大きく異なるアプローチを検討しています。
ソフトウェアトランザクションメモリはコンピュータサイエンスにおける近年の研究成果であり、ロックより良い解決策を提供します。
ここでソフトウェアトランザクションメモリについて、簡単に紹介します。

あなたが"list1"から項目をpopして"list2の'にpopしたものを追加したいとしましょう​​:

def f(list1, list2):
x = list1.pop()
list2.append(x)

これは、マルチスレッドとして安全な例ではありません(GILであるとしても)。
つまり、スレッド1でf(l1, l2)とスレッド2でf(l2, l1)を呼び出した場合です。

あなたが望む動作として、全く効果がないということです。(xは一方のリストからもう一方のリストまで動かされた後、元に戻ります)
しかし、それは、二つのリストのトップは入れ替わる代わりに、タイミングの問題によって起こることがあります。

これを治す一つの方法は、グローバルロックを使用することです:

def f(list1, list2):
global_lock.acquire()
x = list1.pop()
list2.append(x)
global_lock.release()

これを修正するより良い方法は、リストが備えているロックを使用することです:

def f(list1, list2):
acquire_all_locks(list1.lock, list2.lock)
x = list1.pop()
list2.append(x)
release_all_locks(list1.lock, list2.lock)

2番目の解決策はJythonのモデルです。そしてもう一方の1番目はCPythonのモデルです。CPythonインタプリタでGILを獲得し、一つのバイトコード(実際は100のような実数)にし、そしてGILを解放します。
そしてさらに、100の次の分岐を続けます。

ソフトウェアトランザクションは3番目の解決策をもたらします:

def f(list1, list2):
while True:
t = transaction()
x = list1.pop(t)
list2.append(t, x)
if t.commit():
break

この解決策では、トランザクションオブジェクトを作成し、すべてのリストへの読み書きで使用します。
それはは実際にいくつかの異なるモデルがありますが、そのうちの一つに注目してみましょう。
トランザクション中に、我々は実際にはまったくもってグローバルメモリを変更しません。その代わりに、スレッドローカルトランザクションオブジェクトを使用します


我々はそのオブジェクトに、どれから読むか、どのオブジェクトに書くか、何の値を書くか格納します。
それは、トランザクションに達した時、私たちはコミットの試みに達するのみです。
コミットは失敗するかもしれません。もし他のコミットがその間に発生した場合、矛盾が発生します。その場合、トランザクションは中止し、再度はじめから処理をやり直さなければなりません。

前の2つの解決策がCPythonとJythonのモデルであるのと同様に、STMの解決策は、PyPyの将来におけるモデルであるかのように見えます。
PyPyインタプリタは、トランザクションを開始、複数のバイトコードを実行、トランザクションを終了という処理をとてもよく繰り返します。
これはCPythonのGILが行っていることと大変似通っています。
特に、この処理はプログラマがGILの場合と同様に、同一の保証を与えられることを意味します。
唯一の違いは、コードがお互いを妨げない限り、マルチスレッドで並列に実行できるということです。
(とりわけ、あなたが既存のマルチスレッド化されたプログラムにおいて、GILだけではなく実際のロックも必要とするなら、これは不思議にもそれらの必要性を削除しません。
あなたがロックよりそれを好むなら、PythonプログラムにSTMを触れさせる特別な組み込みモジュールを取得する可能性がありますが、それは別の質問です)

なぜそのアイデアをCPythonに適用しないのでしょうか?
なぜならば、私たちはいたるところを変更する必要があるからでしょう。先程の例で説明すれば、
私はもう 'list1.pop()'と呼出しをしません。必ず呼出しは'list1.pop(t)'です。これはすべて実装を"トランザクション的な"動きを行うために、変える必要があると言うことです。

これは、代わりに実際にリストが格納されているグローバルメモリを変更することを意味します。つまり、代わりのトランザクションのオブジェクトの変更を記録しなければなりません。
私たちのインタプリタがCPythonのようにC言語で記述されている場合、我々は、明示的にいたるところでそれを書く必要があります。
それはより高いレベルの言語ではなく、書かれている場合など、変換ルールのセットとして、PyPyはそのまま、次に我々はこの動作を追加することができ、それが必要な場所であればどこでもそれらを自動的に適用されます。

それがPyPyのようなより高いレベルの言語で代わりに記述されているのであれば、
我々は変換規則のセットとして、この振舞いを加えることができますとともに、自動的にどこにでも、変換規則が必要であるところに振る舞いを適用します。
また、変換時のオプションを指定することができます。
あなたはGILがある現在の"pypy"、または、余分な出力処理のためより遅いでしょうがSTMがあるバージョンを得ることができます。
(どのくらい遅いかって?私はさっぱりわからないけど、荒っぽい推測として、2〜5倍程度遅いかもしれません。
十分なコアがある場合は、その限りスケールしすばらしいですよ。 :-)

最終的な注記:STMの研究は、ごく真新しく(2003年頃開始)、多くの亜種があり、どの場合にどれがより優れているかは、まだ明らかではありません。
私が判断できる限り、"A Comprehensive Strategy for Contention Management in Software Transactional Memory"(日本語注記:ソフトウェアの取引のメモリにおける主張管理のための包括的戦略)で説明されたアプローチは、1つが最先端であるということであるように思えます。それはまた、"すべてのケースに十分良い"ように思えます。

それで、いつグローバルインタプリタを取り除くことをするのでしょうか?私はまだ明言することができません。
それはまだアイデアの段階ですが、私はそれが実現できると思います。それを書くには私たちはどのくらいの時間を取るでしょうか?
再びさっぱりわかりません、しかし、我々は何日ではなく何ヶ月であると調べています。
これは私がユーロの資金が9月1日がなくなった後も、フルタイムで作業できるようにしたいと思ったりなど...。

現在、私たちは私が着手できるように金を工面するのにcrowdfundingを使用する方法を調べています。 とってもすぐにブログ記事のポストを期待していてください。
しかし、これはcrowdfundingするうってつけの候補者に似ています。 --GILを無くすために10ユーロを支払うことをいとわない何千人ものあなた方が一応は存在します。今、私たちはこれを実現するだけです。

(原文:Posted by Armin Rigo)

0 件のコメント:

コメントを投稿